PageRenderTime 35ms CodeModel.GetById 23ms app.highlight 9ms RepoModel.GetById 0ms app.codeStats 0ms

/bcbansapi.py

https://bitbucket.org/Lavode/bcbans-api
Python | 289 lines | 228 code | 20 blank | 41 comment | 22 complexity | 81c75ac9064e7e07ce3b3b129a7c52db MD5 | raw file
  1# Copyright (C) 2012 Michael Senn "Morrolan"
  2#
  3# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
  4# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
  5# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
  6# persons to whom the Software is furnished to do so, subject to the following conditions:
  7# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
  8# Software.
  9
 10# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 11# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 12# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 13# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 14
 15# API documentation: http://wiki.ecs-server.net/display/BCBANS/API
 16# Better API documentation: http://www.bcbans.com/api
 17# API implementation by plugin: https://bitbucket.org/aetaric/bcbansplugin/src/275a3fb9d4ed/src/me/jabjabjab/plugins/bcbans/API.java
 18# IMPORTANT: Those three conflict. Search for: 'TODO: API:Conflict'
 19
 20from urllib.request import urlopen
 21from urllib.parse import urlencode
 22from urllib.error import URLError, HTTPError
 23import socket
 24import json
 25import logging
 26import re
 27
 28# Todo: API is there a player callback function?
 29# I find it strange that there is no player callback function. Is that done via the query? If so: Supplying an IP there is broken.
 30# Update: 09.09.12 according to aetaric on IRC there is none, but it does use the IP supplied via the ban function to do alt detection.
 31# Most likely also uses the one supplied via query, if it wouldn't break.
 32# Update 11.09.2012 api docs on http://www.bcbans.com/api claim there is one, but it 404s. Suppose not implemented yet.
 33
 34# Todo: API add support for different locale.
 35
 36# Todo: Docstrings
 37
 38class APIManager(object):
 39    _APIPOSTURL = "http://www.bcbans.com/api/{0}.{1}"
 40
 41    _VALID_BAN_TYPES = {
 42        "local": 0,
 43        "global": 1
 44    }
 45
 46    REGEX_APIKEY = "[0-9|A-F|a-f]{40]"
 47
 48    def __init__(self, apikey=None, api_post_url=None):
 49        self._apikey = apikey
 50        # If an alternative API URL was supplied we'll use that one, else we'll stick with the default one.
 51        if api_post_url is not None:
 52            self._APIPOSTURL = api_post_url
 53        else:
 54            self._APIPOSTURL = APIManager._APIPOSTURL
 55
 56        # Sets the timeout to something reasonable.
 57        socket.setdefaulttimeout(10)
 58
 59        # Compiling the RegEx
 60        self._c_regex_apikey = re.compile(APIManager.REGEX_APIKEY)
 61
 62    def _get_api_key(self):
 63        return self._apikey
 64
 65    def _set_api_key(self, value):
 66        if not self._c_regex_apikey.match(str(value)):
 67            logging.warning("API key might be invalid {}".format(value))
 68        self._apikey = str(value)
 69
 70    apikey = property(_get_api_key, _set_api_key)
 71
 72
 73
 74    def add_player_ban(self, issuer, player, reason, type=0, ip=None):
 75        """Bans the specified player.
 76
 77        Parameters:
 78        issuer -- The admin who issued the ban.
 79        player -- The player whom to ban.
 80        reason -- The ban reason.
 81        type=0 -- 0 or 'local' for a local ban, 1 or 'global' for a global ban.
 82        ip==None -- If set to anything other than none, it'll be used for alt detection. It will, however, not ban the IP.
 83
 84        Returns:
 85        dict -- {"player_id": int, "server_id": int, "dispute_id": int/None, "issuer": str, "reason": str, "created_at": ISO-8601 string, "updated_at": ISO-8601 string, "global": bool, "id": 87}
 86
 87        Exceptions:
 88        APIError -- Will be raised if the API call failed.
 89        """
 90
 91        # IP Bans: When supplying an IP, the IP is used for alt detection, but it does NOT ban the IP.
 92
 93        if type in APIManager._VALID_BAN_TYPES.values():
 94            pass
 95        elif type in APIManager._VALID_BAN_TYPES:
 96            type = APIManager._VALID_BAN_TYPES[type]
 97        else:
 98            raise ValueError("Invalid ban type supplied.")
 99
100        command = "ban_player"
101        d = {"playername": str(player),
102                "sender": str(issuer),
103                "reason": str(reason)}
104        if type == 1:
105            d["global"] = type
106        if ip is not None:
107            d["ip"] = str(ip)
108
109        result = self._do_api_post_request(command, d)
110
111        if "error" in result:
112            raise APIError(d, result, "Error occurred while trying to add a player ban.")
113
114        return result
115
116    def add_global_player_ban(self, issuer, player, reason, ip=None):
117        """Globally bans the specified player.
118
119        Parameters:
120        issuer -- The admin who issued the ban.
121        player -- The player whom to ban.
122        reason -- The ban reason.
123        ip==None -- If set to anything other than none, it'll be used for alt detection. It will, however, not ban the IP.
124
125        Returns:
126        dict -- {"player_id": int, "server_id": int, "dispute_id": int/None, "issuer": str, "reason": str, "created_at": ISO-8601 string, "updated_at": ISO-8601 string, "global": bool, "id": 87}
127
128        Exceptions:
129        APIError -- Will be raised if the API call failed.
130        """
131        return self.add_player_ban(issuer, player, reason, 1, ip)
132
133    def add_local_player_ban(self, issuer, player, reason, ip=None):
134        """Locally bans the specified player.
135
136        Parameters:
137        issuer -- The admin who issued the ban.
138        player -- The player whom to ban.
139        reason -- The ban reason.
140        ip==None -- If set to anything other than none, it'll be used for alt detection. It will, however, not ban the IP.
141
142        Returns:
143        dict -- {"player_id": int, "server_id": int, "dispute_id": int/None, "issuer": str, "reason": str, "created_at": ISO-8601 string, "updated_at": ISO-8601 string, "global": bool, "id": 87}
144
145        Exceptions:
146        APIError -- Will be raised if the API call failed.
147        """
148        return self.add_player_ban(issuer, player, reason, 0, ip)
149
150
151    def remove_player_ban(self, player):
152        command = "unban_player"
153        d = {"playername": player}
154
155        result = self._do_api_post_request(command, d)
156
157        if "error" in result:
158            raise APIError(d, result, "Error occurred while trying to add a player ban.")
159
160        if result["message"] == "Ban Removed":
161            return True
162        return False
163
164
165    def get_player_bans(self, player, ip=None):
166        command = "query"
167        d = {"playername": player}
168
169        if ip is not None:
170            d["ip"] = str(ip)
171
172        result = self._do_api_post_request(command, d)
173
174        if "error" in result:
175            raise APIError(d, result, "Error occurred while trying to query for a player's bans.")
176
177        return result
178
179    def get_player_ban(self, player, ip=None):
180        command = "check"
181        d = {"playername": player}
182
183        if ip is not None:
184            d["ip"] = str(ip)
185
186        result = self._do_api_post_request(command, d)
187
188        if "error" in result:
189            raise APIError(d, result, "Error occurred while trying to check for a player's bans.")
190
191        return result
192
193    def get_server_info(self, id):
194        command = "server_lookup"
195
196        d = {"server_id": id}
197
198        result = self._do_api_post_request(command, d)
199
200        if "error" in result:
201            raise APIError(d, result, "Error occurred while trying to retrieve server information.")
202
203        return result
204
205    def get_site_statistics(self):
206        command = "site/stats"
207
208        result = self._do_api_get_request(command)
209
210        if "error" in result:
211            raise APIError(None, result, "Error occurred while trying to retrieve site statistics.")
212
213
214    def _do_api_post_request(self, command, data, type="json"):
215
216        if self._apikey is None:
217            logging.exception("Can not issue API request without an API key! Aborting now.")
218            return
219
220        result_json = None
221
222        data["apikey"] = self._apikey
223        url = self._APIPOSTURL.format(command, type)
224        post_data_encoded = urlencode(data).encode("utf-8")
225
226        try:
227            response = urlopen(url, post_data_encoded)
228            result_json = response.read().decode("utf-8")
229            result = json.loads(result_json)
230        except HTTPError as http_ex:
231            logging.exception("HTTPError occurred while querying the API.", http_ex.errno, http_ex.reason, sep="\n")
232            raise APIError(data, result_json, "HTTPError occurred while querying the API")
233        except URLError as url_ex:
234            logging.exception("URLError occurred while querying the API.", url_ex.reason, sep="\n")
235            raise APIError(data, result_json, "Error occurred while querying the API")
236        except UnicodeDecodeError as uni_ex:
237            logging.exception("UnicodeDecodeError occurred while decoding the API's response.", uni_ex.reason, sep="\n")
238            raise APIError(data, result_json, "Error occurred while decoding the API response")
239        except ValueError as val_ex:
240            logging.exception("ValueError occurred while parsing the JSON.", val_ex.args, sep="\n")
241            raise APIError(data, result_json, "Error occurred while parsing the JSON.")
242
243        return result
244
245    def _do_api_get_request(self, command, type="json"):
246        if self._apikey is None:
247            logging.exception("Can not issue API request without an API key! Aborting now.")
248            return
249
250        result_json = None
251
252        url = self._APIPOSTURL.format(command, type)
253
254        try:
255            response = urlopen(url)
256            result_json = response.read().decode("utf-8")
257            result = json.loads(result_json)
258        except HTTPError as http_ex:
259            logging.exception("HTTPError occurred while querying the API.", http_ex.errno, http_ex.reason, sep="\n")
260            raise APIError(None, result_json, "HTTPError occurred while querying the API")
261        except URLError as url_ex:
262            logging.exception("URLError occurred while querying the API.", url_ex.reason, sep="\n")
263            raise APIError(None, result_json, "Error occurred while querying the API")
264        except UnicodeDecodeError as uni_ex:
265            logging.exception("UnicodeDecodeError occurred while decoding the API's response.", uni_ex.reason, sep="\n")
266            raise APIError(None, result_json, "Error occurred while decoding the API response")
267        except ValueError as val_ex:
268            logging.exception("ValueError occurred while parsing the JSON.", val_ex.args, sep="\n")
269            raise APIError(None, result_json, "Error occurred while parsing the JSON.")
270
271        return result
272
273
274class APIError(Exception):
275    def __init__(self, query, result, description=None, *args, **kwargs):
276        super().__init__(*args, **kwargs)
277        self.query = query
278        self.query["apikey"] = "***redacted***"
279        self.result = result
280        self.description = description
281
282
283
284
285
286
287
288
289