def create(session: Session, steam_id: int): d = session.query(Dota).get(steam_id) if d is not None: raise AlreadyExistingError(repr(d)) r = requests.get( f"https://api.opendota.com/api/players/{Steam.to_steam_id_3(steam_id)}" ) if r.status_code != 200: raise RequestError("OpenDota returned {r.status_code}") data = r.json() if "profile" not in data: raise NotFoundError( "The specified user has never played Dota or has a private match history" ) r = requests.get( f"https://api.opendota.com/api/players/{Steam.to_steam_id_3(steam_id)}/wl" ) if r.status_code != 200: raise RequestError("OpenDota returned {r.status_code}") wl = r.json() new_record = Dota(steam_id=str(steam_id), rank_tier=data["rank_tier"], wins=wl["win"], losses=wl["lose"]) return new_record
def create(session: Session, royal_id, battletag, discriminator=None): if discriminator is None: battletag, discriminator = battletag.split("#", 1) o = session.query(Overwatch).filter_by( battletag=battletag, discriminator=discriminator).first() if o is not None: raise AlreadyExistingError(repr(o)) r = requests.get( f"https://owapi.net/api/v3/u/{battletag}-{discriminator}/stats", headers={ "User-Agent": "Royal-Bot/4.0", "From": "*****@*****.**" }) if r.status_code != 200: raise RequestError(f"OWAPI.net returned {r.status_code}") try: j = r.json()["eu"]["stats"]["quickplay"]["overall_stats"] except TypeError: raise RequestError( "Something went wrong when retrieving the stats.") o = Overwatch( royal_id=royal_id, battletag=battletag, discriminator=discriminator, icon=re.search( r"https://.+\.cloudfront\.net/game/unlocks/(0x[0-9A-F]+)\.png", j["avatar"]).group(1), level=j["prestige"] * 100 + j["level"], rank=j["comprank"]) return o
def login(self, apikey): result = self.send_request('login', {'apikey': apikey}) if result is None: raise RequestError('No result after login') session = result.get('session') if not session: raise RequestError('No session in result') self.session = session
def update(self): r = requests.get( f"https://api.opendota.com/api/players/{Steam.to_steam_id_3(self.steam_id)}" ) if r.status_code != 200: raise RequestError("OpenDota returned {r.status_code}") data = r.json() r = requests.get( f"https://api.opendota.com/api/players/{Steam.to_steam_id_3(self.steam_id)}/wl" ) if r.status_code != 200: raise RequestError("OpenDota returned {r.status_code}") wl = r.json() self.rank_tier = data["rank_tier"] self.wins = wl["win"] self.losses = wl["lose"]
def _api_request(self, data): params = {'id': self.sequence_num} self.sequence_num += 1 if self.sid: params.update({'sid': self.sid}) # ensure input data is a list if not isinstance(data, list): data = [data] url = f'{self.schema}://g.api.{self.domain}/cs' req = requests.post( url, params=params, data=json.dumps(data), timeout=self.timeout, ) json_resp = json.loads(req.text) if isinstance(json_resp, int): if json_resp == -3: msg = 'Request failed, retrying' logger.info(msg) raise RuntimeError(msg) raise RequestError(json_resp) return json_resp[0]
def create(session: Session, royal_id, summoner_name=None, summoner_id=None): if summoner_name: lol = session.query(LeagueOfLegends).filter( LeagueOfLegends.summoner_name == summoner_name).first() elif summoner_id: lol = session.query(LeagueOfLegends).get(summoner_id) else: raise SyntaxError( "Neither summoner_name or summoner_id are specified") if lol is not None: raise AlreadyExistingError(repr(lol)) # Get the summoner_id if summoner_name: r = requests.get( f"https://euw1.api.riotgames.com/lol/summoner/v3/summoners/by-name/{summoner_name}?api_key={config['League of Legends']['riot_api_key']}" ) else: r = requests.get( f"https://euw1.api.riotgames.com/lol/summoner/v3/summoners/{summoner_id}?api_key={config['League of Legends']['riot_api_key']}" ) if r.status_code != 200: return RequestError( f"League of Legends API returned {r.status_code}") data = r.json() lol = LeagueOfLegends(royal_id=royal_id, summoner_id=data["id"], summoner_name=data["name"], level=data["summonerLevel"]) lol.update() return lol
def create(session: Session, royal_id, osu_name): o = session.query(Osu).filter(Osu.osu_name == osu_name).first() if o is not None: raise AlreadyExistingError(repr(o)) r0 = requests.get( f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={osu_name}&m=0" ) r1 = requests.get( f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={osu_name}&m=1" ) r2 = requests.get( f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={osu_name}&m=2" ) r3 = requests.get( f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={osu_name}&m=3" ) if r0.status_code != 200 or r1.status_code != 200 or r2.status_code != 200 or r3.status_code != 200: raise RequestError( f"Osu! API returned an error ({r0.status_code} {r1.status_code} {r2.status_code} {r3.status_code})" ) j0 = r0.json()[0] j1 = r1.json()[0] j2 = r2.json()[0] j3 = r3.json()[0] new_record = Osu(royal_id=royal_id, osu_id=j0["user_id"], osu_name=j0["username"], std_pp=j0["pp_raw"], taiko_pp=j1["pp_raw"], catch_pp=j2["pp_raw"], mania_pp=j3["pp_raw"]) return new_record
def update(self): r0 = requests.get( f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={self.osu_name}&m=0" ) r1 = requests.get( f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={self.osu_name}&m=1" ) r2 = requests.get( f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={self.osu_name}&m=2" ) r3 = requests.get( f"https://osu.ppy.sh/api/get_user?k={config['Osu!']['ppy_api_key']}&u={self.osu_name}&m=3" ) if r0.status_code != 200 or r1.status_code != 200 or r2.status_code != 200 or r3.status_code != 200: raise RequestError( f"Osu! API returned an error ({r0.status_code} {r1.status_code} {r2.status_code} {r3.status_code})" ) j0 = r0.json()[0] j1 = r1.json()[0] j2 = r2.json()[0] j3 = r3.json()[0] self.osu_name = j0["username"] self.std_pp = j0["pp_raw"] self.taiko_pp = j1["pp_raw"] self.catch_pp = j2["pp_raw"] self.mania_pp = j3["pp_raw"]
def get_public_file_info(self, file_handle, file_key): """ Get size and name of a public file """ data = self._api_request({'a': 'g', 'p': file_handle, 'ssm': 1}) #if numeric error code response if isinstance(data, int): raise RequestError(data) if 'at' not in data or 's' not in data: raise ValueError("Unexpected result", data) key = base64_to_a32(file_key) k = (key[0] ^ key[4], key[1] ^ key[5], key[2] ^ key[6], key[3] ^ key[7]) size = data['s'] unencrypted_attrs = decrypt_attr(base64_url_decode(data['at']), k) if not unencrypted_attrs: return None result = {'size': size, 'name': unencrypted_attrs['n']} return result
def _parse_url(self, url): # parse file id and key from url if '!' in url: match = re.findall(r'/#!(.*)', url) path = match[0] return path else: raise RequestError('Url key missing')
def _login_user(self, email, password): password_aes = prepare_key(str_to_a32(password)) uh = stringhash(email, password_aes) resp = self._api_request({'a': 'us', 'user': email, 'uh': uh}) #if numeric error code response if isinstance(resp, int): raise RequestError(resp) self._login_process(resp, password_aes)
def update(self): r = requests.get( f"https://euw1.api.riotgames.com/lol/summoner/v3/summoners/{self.summoner_id}?api_key={config['League of Legends']['riot_api_key']}" ) if r.status_code != 200: return RequestError( f"League of Legends API returned {r.status_code}") data = r.json() r = requests.get( f"https://euw1.api.riotgames.com/lol/league/v3/positions/by-summoner/{self.summoner_id}?api_key={config['League of Legends']['riot_api_key']}" ) if r.status_code != 200: return RequestError( f"League of Legends API returned {r.status_code}") rank = r.json() solo_rank = None flex_rank = None twtr_rank = None for league in rank: if league["queueType"] == "RANKED_SOLO_5x5": solo_rank = league elif league["queueType"] == "RANKED_FLEX_SR": flex_rank = league elif league["queueType"] == "RANKED_FLEX_TT": twtr_rank = league self.summoner_id = data["id"] self.summoner_name = data["name"] self.level = data["summonerLevel"] if solo_rank is not None: self.solo_division = LeagueOfLegendsRanks[solo_rank["tier"]] self.solo_rank = RomanNumerals[solo_rank["rank"]] else: self.solo_division = None self.solo_rank = None if flex_rank is not None: self.flex_division = LeagueOfLegendsRanks[flex_rank["tier"]] self.flex_rank = RomanNumerals[flex_rank["rank"]] else: self.flex_division = None self.flex_rank = None if twtr_rank is not None: self.twtr_division = LeagueOfLegendsRanks[twtr_rank["tier"]] self.twtr_rank = RomanNumerals[twtr_rank["rank"]] else: self.twtr_division = None self.twtr_rank = None
def _raise_for_remote_status(url, data): """Raise an error from the remote API if necessary.""" if data.get("errorType") and data["errorType"] > 0: raise_remote_error(data["errorType"]) if data.get("statusCode") and data["statusCode"] != 200: raise RequestError( "Error requesting data from {}: {} {}".format(url, data['statusCode'], data['message']) )
def update(self): r = requests.get( f"https://owapi.net/api/v3/u/{self.battletag}-{self.discriminator}/stats", headers={ "User-Agent": "Royal-Bot/4.0", "From": "*****@*****.**" }) if r.status_code != 200: raise RequestError(f"OWAPI.net returned {r.status_code}") try: j = r.json()["eu"]["stats"]["quickplay"]["overall_stats"] except TypeError: raise RequestError( "Something went wrong when retrieving the stats.") try: self.icon = re.search( r"https://.+\.cloudfront\.net/game/unlocks/(0x[0-9A-F]+)\.png", j["avatar"]).group(1) except AttributeError: pass self.level = j["prestige"] * 100 + j["level"] self.rank = j["comprank"]
def update(self): r = requests.get( f"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={config['Steam']['api_key']}&steamids={self.steam_id}" ) if r.status_code != 200: raise RequestError(f"Steam returned {r.status_code}") j = r.json() self.persona_name = j["response"]["players"][0]["personaname"] self.avatar_hex = re.search( r"https://steamcdn-a\.akamaihd\.net/steamcommunity/public/images/avatars/../(.+).jpg", j["response"]["players"][0]["avatar"]).group(1) r = requests.get( f"http://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v0001/?key={config['Steam']['api_key']}&steamid={self.steam_id}&format=json" ) if r.status_code != 200: raise RequestError(f"Steam returned {r.status_code}") j = r.json() if "response" not in j \ or "games" not in j["response"] \ or len(j["response"]["games"]) < 1: raise RequestError(f"Game data is private") self.most_played_game_id = j["response"]["games"][0]["appid"]
def get_link(self, file): """ Get a file public link from given file object """ file = file[1] if 'h' in file and 'k' in file: public_handle = self._api_request({'a': 'l', 'n': file['h']}) if public_handle == -11: raise RequestError("Can't get a public link from that file " "(is this a shared file?)") decrypted_key = a32_to_base64(file['key']) return (f'{self.schema}://{self.domain}' f'/#!{public_handle}!{decrypted_key}') else: raise ValidationError('File id and key must be present')
def get_folder_link(self, file): try: file = file[1] except (IndexError, KeyError): pass if 'h' in file and 'k' in file: public_handle = self._api_request({'a': 'l', 'n': file['h']}) if public_handle == -11: raise RequestError("Can't get a public link from that file " "(is this a shared file?)") decrypted_key = a32_to_base64(file['shared_folder_key']) return (f'{self.schema}://{self.domain}' f'/#F!{public_handle}!{decrypted_key}') else: raise ValidationError('File id and key must be present')
def request(self, method, url, access_token=None, access_token_expiration=None, ssl=True, **kwargs): """Make a request against the RainMachine device.""" if access_token_expiration and datetime.now() >= access_token_expiration: raise TokenExpiredError("Long-lived access token has expired") kwargs.setdefault('headers', {"Content-Type": "application/json"}) if access_token: kwargs.setdefault('params', {})['access_token'] = access_token try: resp = requests.request(method, url, verify=ssl, timeout=DEFAULT_TIMEOUT, **kwargs) resp.raise_for_status() data = resp.json() _raise_for_remote_status(url, data) except requests.exceptions.HTTPError: raise TokenExpiredError("Long-lived access token has expired") except requests.exceptions.Timeout: raise RequestError("HTTP error occurred using url: {}".format(url)) return data
def create(session: Session, royal_id: int, steam_id: str): s = session.query(Steam).get(steam_id) if s is not None: raise AlreadyExistingError(repr(s)) r = requests.get( f"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={config['Steam']['api_key']}&steamids={steam_id}" ) if r.status_code != 200: raise RequestError(f"Steam returned {r.status_code}") j = r.json() if len(j) == 0: raise NotFoundError( f"The steam_id doesn't match any steam account") s = Steam( royal_id=royal_id, steam_id=steam_id, persona_name=j["response"]["players"][0]["personaname"], avatar_hex=re.search( r"https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/../(.+).jpg", j["response"]["players"][0]["avatar"]).group(1)) return s
def login_anonymous(self): logger.info('Logging in anonymous temporary user...') master_key = [random.randint(0, 0xFFFFFFFF)] * 4 password_key = [random.randint(0, 0xFFFFFFFF)] * 4 session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4 user = self._api_request({ 'a': 'up', 'k': a32_to_base64(encrypt_key(master_key, password_key)), 'ts': base64_url_encode( a32_to_str(session_self_challenge) + a32_to_str(encrypt_key(session_self_challenge, master_key))) }) resp = self._api_request({'a': 'us', 'user': user}) if isinstance(resp, int): raise RequestError(resp) self._login_process(resp, password_key)
def _api_request(self, data): params = {'id': self.sequence_num} self.sequence_num += 1 if self.sid: params.update({'sid': self.sid}) #ensure input data is a list if not isinstance(data, list): data = [data] req = requests.post('{0}://g.api.{1}/cs'.format( self.schema, self.domain), params=params, data=json.dumps(data), timeout=self.timeout) json_resp = json.loads(req.text) #if numeric error code response if isinstance(json_resp, int): raise RequestError(json_resp) return json_resp[0]
def make_call(self, request_name, env_name): request = self.requests.find(request_name) environment = self.environments.find(env_name) parsed_req = self._parse_request(request, env_name) url = f"{environment['base_url']}{parsed_req['endpoint']}" headers = {'content-type': 'application/json'} if 'headers' in environment: headers = {**headers, **environment['headers']} if parsed_req['type'] == 'POST': response = requests.post(url, data=json.dumps(parsed_req['body']), headers=headers) elif parsed_req['type'] == 'PUT': response = requests.put(url, data=json.dumps(parsed_req['body']), headers=headers) elif parsed_req['type'] == 'GET': response = requests.get(url, params=parsed_req['body'], headers=headers) else: raise UserError(f'Unknown HTTP method {parsed_req["type"]}') response_json = response.json() if response.status_code != 200: raise RequestError( f'{response.status_code} returned when calling {request_name} with response ' f'{response_json}. Expected status code 200.') if self.print_all_responses: print(f'Response for call to {request_name}:') print(response_json) return response_json
def _login_user(self, email, password): logger.info('Logging in user...') email = email.lower() get_user_salt_resp = self._api_request({'a': 'us0', 'user': email}) user_salt = None try: user_salt = base64_to_a32(get_user_salt_resp['s']) except KeyError: # v1 user account password_aes = prepare_key(str_to_a32(password)) user_hash = stringhash(email, password_aes) else: # v2 user account pbkdf2_key = hashlib.pbkdf2_hmac(hash_name='sha512', password=password.encode(), salt=a32_to_str(user_salt), iterations=100000, dklen=32) password_aes = str_to_a32(pbkdf2_key[:16]) user_hash = base64_url_encode(pbkdf2_key[-16:]) resp = self._api_request({'a': 'us', 'user': email, 'uh': user_hash}) if isinstance(resp, int): raise RequestError(resp) self._login_process(resp, password_aes)
def send_request(self, service, args={}, file_args=None): ''' service: string args: dict ''' if self.session is not None: args.update({'session': self.session}) print 'Python:', args json = python2json(args) print 'Sending json:', json url = self.get_url(service) print 'Sending to URL:', url # If we're sending a file, format a multipart/form-data if file_args is not None: m1 = MIMEBase('text', 'plain') m1.add_header('Content-disposition', 'form-data; name="request-json"') m1.set_payload(json) m2 = MIMEApplication(file_args[1], 'octet-stream', encode_noop) m2.add_header( 'Content-disposition', 'form-data; name="file"; filename="%s"' % file_args[0]) #msg.add_header('Content-Disposition', 'attachment', # filename='bud.gif') #msg.add_header('Content-Disposition', 'attachment', # filename=('iso-8859-1', '', 'FuSballer.ppt')) mp = MIMEMultipart('form-data', None, [m1, m2]) # Makie a custom generator to format it the way we need. from cStringIO import StringIO from email.generator import Generator class MyGenerator(Generator): def __init__(self, fp, root=True): Generator.__init__(self, fp, mangle_from_=False, maxheaderlen=0) self.root = root def _write_headers(self, msg): # We don't want to write the top-level headers; # they go into Request(headers) instead. if self.root: return # We need to use \r\n line-terminator, but Generator # doesn't provide the flexibility to override, so we # have to copy-n-paste-n-modify. for h, v in msg.items(): print >> self._fp, ('%s: %s\r\n' % (h, v)), # A blank line always separates headers from body print >> self._fp, '\r\n', # The _write_multipart method calls "clone" for the # subparts. We hijack that, setting root=False def clone(self, fp): return MyGenerator(fp, root=False) fp = StringIO() g = MyGenerator(fp) g.flatten(mp) data = fp.getvalue() headers = {'Content-type': mp.get('Content-type')} if False: print 'Sending headers:' print ' ', headers print 'Sending data:' print data[:1024].replace('\n', '\\n\n').replace('\r', '\\r') if len(data) > 1024: print '...' print data[-256:].replace('\n', '\\n\n').replace('\r', '\\r') print else: # Else send x-www-form-encoded data = {'request-json': json} print 'Sending form data:', data data = urlencode(data) print 'Sending data:', data headers = {} request = Request(url=url, headers=headers, data=data) try: f = urlopen(request) txt = f.read() print 'Got json:', txt result = json2python(txt) print 'Got result:', result stat = result.get('status') print 'Got status:', stat if stat == 'error': errstr = result.get('errormessage', '(none)') raise RequestError('server error message: ' + errstr) return result except HTTPError, e: print 'HTTPError', e
def send_request(self, service, args={}, file_args=None): if self.session is not None: args.update({'session': self.session}) json = python2json(args) url = self.get_url(service) # If we're sending a file, format a multipart/form-data if file_args is not None: m1 = MIMEBase('text', 'plain') m1.add_header('Content-disposition', 'form-data; name="request-json"') m1.set_payload(json) m2 = MIMEApplication(file_args[1], 'octet-stream', encode_noop) m2.add_header( 'Content-disposition', 'form-data; name="file"; filename="%s"' % file_args[0]) mp = MIMEMultipart('form-data', None, [m1, m2]) # Make a custom generator to format it the way we need. from cStringIO import StringIO from email.generator import Generator class MyGenerator(Generator): def __init__(self, fp, root=True): Generator.__init__(self, fp, mangle_from_=False, maxheaderlen=0) self.root = root def _write_headers(self, msg): # We don't want to write the top-level headers; # they go into Request(headers) instead. if self.root: return # We need to use \r\n line-terminator, but Generator # doesn't provide the flexibility to override, so we # have to copy-n-paste-n-modify. for h, v in msg.items(): self._fp.write('%s: %s\r\n' % (h, v)) # A blank line always separates headers from body self._fp.write('\r\n') # The _write_multipart method calls "clone" for the # sub-parts. We hijack that, setting root=False. def clone(self, fp): return MyGenerator(fp, root=False) fp = StringIO() g = MyGenerator(fp) g.flatten(mp) data = fp.getvalue() headers = {'Content-type': mp.get('Content-type')} else: # Else send x-www-form-encoded data = {'request-json': json} data = urlencode(data) headers = {} request = Request(url=url, headers=headers, data=data) response = urlopen(request, timeout=30) text = response.read() result = json2python(text) status = result.get('status') if status == 'error': error_message = result.get('errormessage', '(none)') log.error("Astrometry.net request error: %s" % error_message) raise RequestError('Server error message: ' + error_message) return result
def _download_file(self, file_handle, file_key, dest_path=None, dest_filename=None, is_public=False, file=None): if file is None: if is_public: file_key = base64_to_a32(file_key) file_data = self._api_request({ 'a': 'g', 'g': 1, 'p': file_handle }) else: file_data = self._api_request({ 'a': 'g', 'g': 1, 'n': file_handle }) k = (file_key[0] ^ file_key[4], file_key[1] ^ file_key[5], file_key[2] ^ file_key[6], file_key[3] ^ file_key[7]) iv = file_key[4:6] + (0, 0) meta_mac = file_key[6:8] else: file_data = self._api_request({'a': 'g', 'g': 1, 'n': file['h']}) k = file['k'] iv = file['iv'] meta_mac = file['meta_mac'] # Seems to happens sometime... When this occurs, files are # inaccessible also in the official also in the official web app. # Strangely, files can come back later. if 'g' not in file_data: raise RequestError('File not accessible anymore') file_url = file_data['g'] file_size = file_data['s'] attribs = base64_url_decode(file_data['at']) attribs = decrypt_attr(attribs, k) if dest_filename is not None: file_name = dest_filename else: file_name = attribs['n'] input_file = requests.get(file_url, stream=True).raw if dest_path is None: dest_path = '' else: dest_path += '/' with tempfile.NamedTemporaryFile(mode='w+b', prefix='megapy_', delete=False) as temp_output_file: k_str = a32_to_str(k) counter = Counter.new(128, initial_value=((iv[0] << 32) + iv[1]) << 64) aes = AES.new(k_str, AES.MODE_CTR, counter=counter) mac_str = '\0' * 16 mac_encryptor = AES.new(k_str, AES.MODE_CBC, mac_str.encode("utf8")) iv_str = a32_to_str([iv[0], iv[1], iv[0], iv[1]]) for chunk_start, chunk_size in get_chunks(file_size): chunk = input_file.read(chunk_size) chunk = aes.decrypt(chunk) temp_output_file.write(chunk) encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) for i in range(0, len(chunk) - 16, 16): block = chunk[i:i + 16] encryptor.encrypt(block) # fix for files under 16 bytes failing if file_size > 16: i += 16 else: i = 0 block = chunk[i:i + 16] if len(block) % 16: block += b'\0' * (16 - (len(block) % 16)) mac_str = mac_encryptor.encrypt(encryptor.encrypt(block)) file_info = os.stat(temp_output_file.name) logger.info('%s of %s downloaded', file_info.st_size, file_size) file_mac = str_to_a32(mac_str) # check mac integrity if (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) != meta_mac: raise ValueError('Mismatched mac') output_path = Path(dest_path + file_name) shutil.copy(temp_output_file.name, output_path) return output_path