/bcbansapi.py
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