def decrypt_token(action: str, token: str): """ Decrypts a token and returns the data in a list of strings Throws exception when the action does not fit or the token expired :param action: a unique identifier for the action associated with this token :param token: the crypted token :return: list of strings which contains the data given on creating the token """ # F**k you urlsafe_b64encode & padding and f**k you overzealous http implementations token = token.replace('%3d', '=') token = token.replace('%3D', '=') ciphertext = base64.urlsafe_b64decode(token.encode()) plaintext = Fernet(CRYPTO_KEY).decrypt(ciphertext).decode("utf-8") token_action, expiry, *result = plaintext.split(',') if token_action != action: raise ApiException([Error(ErrorCode.TOKEN_INVALID)]) if (float(expiry) < time.time()): raise ApiException([Error(ErrorCode.TOKEN_EXPIRED)]) return result
def mods_upload(): """ Creates a new mod in the system **Example Request**: .. sourcecode:: http POST /mods/upload **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: text/javascript "ok" :query file file: The file submitted (Must be ZIP) :type: zip """ file = request.files.get('file') if not file: raise ApiException([Error(ErrorCode.UPLOAD_FILE_MISSING)]) if not file_allowed(file.filename): raise ApiException([Error(ErrorCode.UPLOAD_INVALID_FILE_EXTENSION, *ALLOWED_EXTENSIONS)]) filename = secure_filename(file.filename) file.save(os.path.join(app.config['MOD_UPLOAD_PATH'], filename)) return "ok"
def validate_email(email: str) -> bool: """ Checks correctness of a username Throws exception if the syntax does not fit, the domain is blacklistet or is already in use :email name: Users email (Case sensitive) :return: true if no error occured """ # check for correct email syntax if not EMAIL_REGEX.match(email): raise ApiException([Error(ErrorCode.INVALID_EMAIL, email)]) # check for blacklisted email domains (we don't like disposable email) with db.connection: cursor = db.connection.cursor() cursor.execute("SELECT lower(domain) FROM email_domain_blacklist") rows = cursor.fetchall() # Get list of blacklisted domains (so we can suffix-match incoming emails # in sublinear time) blacklisted_email_domains = marisa_trie.Trie(map(lambda x: x[0], rows)) domain = email.split("@")[1].lower() if domain in blacklisted_email_domains: raise ApiException([Error(ErrorCode.BLACKLISTED_EMAIL, email)]) # ensue that email adress is unique cursor.execute("SELECT id FROM `login` WHERE LOWER(`email`) = %s", (email.lower(),)) if cursor.fetchone() is not None: raise ApiException([Error(ErrorCode.EMAIL_REGISTERED, email)]) return True
def maps_upload(): """ Creates a new map in the system **Example Request**: .. sourcecode:: http POST /maps/upload **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: text/javascript "ok" :query file file: The file submitted (Must be ZIP) :type: zip """ file = request.files.get('file') metadata_string = request.form.get('metadata') if not file: raise ApiException([Error(ErrorCode.UPLOAD_FILE_MISSING)]) if not metadata_string: raise ApiException([Error(ErrorCode.UPLOAD_METADATA_MISSING)]) if not file_allowed(file.filename): raise ApiException([ Error(ErrorCode.UPLOAD_INVALID_FILE_EXTENSION, *ALLOWED_EXTENSIONS) ]) metadata = json.loads(metadata_string) with tempfile.TemporaryDirectory() as temp_dir: temp_map_path = os.path.join(temp_dir, secure_filename(file.filename)) file.save(temp_map_path) process_uploaded_map(temp_map_path, metadata.get('is_ranked', False)) return "ok"
def update_steps(achievement_id, player_id, steps, steps_function): """Increments the steps of an achievement. This function is NOT an endpoint. :param achievement_id: ID of the achievement to increment :param player_id: ID of the player to increment the achievement for :param steps: The number of steps to increment :param steps_function: The function to use to calculate the new steps value. Two parameters are passed; the current step count and the parameter ``steps`` :return: If successful, this method returns a dictionary with the following structure:: { "current_steps": integer, "current_state": string, "newly_unlocked": boolean, } """ achievement = achievements_get(achievement_id)['data']['attributes'] if achievement['type'] != 'INCREMENTAL': raise ApiException([Error(ErrorCode.ACHIEVEMENT_CANT_INCREMENT_STANDARD, achievement_id)]) with db.connection: cursor = db.connection.cursor(db.pymysql.cursors.DictCursor) cursor.execute("""SELECT current_steps, state FROM player_achievements WHERE achievement_id = %s AND player_id = %s""", (achievement_id, player_id)) player_achievement = cursor.fetchone() new_state = 'REVEALED' newly_unlocked = False current_steps = player_achievement['current_steps'] if player_achievement else 0 new_current_steps = steps_function(current_steps, steps) if new_current_steps >= achievement['total_steps']: new_state = 'UNLOCKED' new_current_steps = achievement['total_steps'] newly_unlocked = player_achievement['state'] != 'UNLOCKED' if player_achievement else True cursor.execute("""INSERT INTO player_achievements (player_id, achievement_id, current_steps, state) VALUES (%(player_id)s, %(achievement_id)s, %(current_steps)s, %(state)s) ON DUPLICATE KEY UPDATE current_steps = VALUES(current_steps), state = VALUES(state)""", { 'player_id': player_id, 'achievement_id': achievement_id, 'current_steps': new_current_steps, 'state': new_state, }) return dict(current_steps=new_current_steps, current_state=new_state, newly_unlocked=newly_unlocked)
def unlock_achievement(achievement_id, player_id): """Unlocks a standard achievement. This function is NOT an endpoint. :param achievement_id: ID of the achievement to unlock :param player_id: ID of the player to unlock the achievement for :return: If successful, this method returns a dictionary with the following structure:: { "newly_unlocked": boolean, } """ newly_unlocked = False with db.connection: cursor = db.connection.cursor(db.pymysql.cursors.DictCursor) cursor.execute( 'SELECT type FROM achievement_definitions WHERE id = %s', achievement_id) achievement = cursor.fetchone() if achievement['type'] != 'STANDARD': raise ApiException([ Error(ErrorCode.ACHIEVEMENT_CANT_UNLOCK_INCREMENTAL, achievement_id) ]) cursor.execute( """SELECT state FROM player_achievements WHERE achievement_id = %s AND player_id = %s""", (achievement_id, player_id)) player_achievement = cursor.fetchone() new_state = 'UNLOCKED' newly_unlocked = not player_achievement or player_achievement[ 'state'] != 'UNLOCKED' cursor.execute( """INSERT INTO player_achievements (player_id, achievement_id, state) VALUES (%(player_id)s, %(achievement_id)s, %(state)s) ON DUPLICATE KEY UPDATE state = VALUES(state)""", { 'player_id': player_id, 'achievement_id': achievement_id, 'state': new_state, }) return dict(newly_unlocked=newly_unlocked)
def validate_mod_info(mod_info): errors = [] name = mod_info.get('name') if not name: errors.append(Error(ErrorCode.MOD_NAME_MISSING)) if len(name) > 100: raise ApiException( [Error(ErrorCode.MOD_NAME_TOO_LONG, 100, len(name))]) if not mod_info.get('uid'): errors.append(Error(ErrorCode.MOD_UID_MISSING)) if not mod_info.get('version'): errors.append(Error(ErrorCode.MOD_VERSION_MISSING)) if not mod_info.get('description'): errors.append(Error(ErrorCode.MOD_DESCRIPTION_MISSING)) if not mod_info.get('author'): errors.append(Error(ErrorCode.MOD_DESCRIPTION_MISSING)) if 'ui_only' not in mod_info: errors.append(Error(ErrorCode.MOD_UI_ONLY_MISSING)) if errors: raise ApiException(errors)
def mods_upload(): """ Uploads a new mod into the system. **Example Request**: .. sourcecode:: http POST /mods/upload **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: text/javascript "ok" :query file file: The file submitted (Must be ZIP) :type: zip """ file = request.files.get('file') if not file: raise ApiException([Error(ErrorCode.UPLOAD_FILE_MISSING)]) if not file_allowed(file.filename): raise ApiException([ Error(ErrorCode.UPLOAD_INVALID_FILE_EXTENSION, *ALLOWED_EXTENSIONS) ]) with tempfile.TemporaryDirectory() as temp_dir: temp_mod_path = os.path.join(temp_dir, secure_filename(file.filename)) file.save(temp_mod_path) process_uploaded_mod(temp_mod_path) return "ok"
def get_page_attributes(max_page_size, request): raw_page_size = request.values.get('page[size]', max_page_size) try: page_size = int(raw_page_size) if page_size > max_page_size: raise ApiException( [Error(ErrorCode.QUERY_INVALID_PAGE_SIZE, page_size)]) except ValueError: raise ApiException( [Error(ErrorCode.QUERY_INVALID_PAGE_SIZE, raw_page_size)]) raw_page = request.values.get('page[number]', 1) try: page = int(raw_page) if page < 1: raise ApiException( [Error(ErrorCode.QUERY_INVALID_PAGE_NUMBER, page)]) except ValueError: raise ApiException( [Error(ErrorCode.QUERY_INVALID_PAGE_NUMBER, raw_page)]) return page, page_size
def validate_username(name: str) -> bool: """ Checks correctness of a username Throws exception if the syntax does not fit or is already in use :param name: FAF username (Case sensitive) :return: true if no error occured """ # check for correct syntax if not USERNAME_REGEX.match(name): raise ApiException([Error(ErrorCode.INVALID_USERNAME, name)]) with db.connection: cursor = db.connection.cursor() # ensure that username is unique cursor.execute("SELECT id FROM `login` WHERE LOWER(`login`) = %s", (name.lower(),)) if cursor.fetchone() is not None: raise ApiException([Error(ErrorCode.USERNAME_TAKEN, name)]) return True
def get_order_by(sort_expression, valid_fields): """ Converts the `sort_expression` into an "order by" if all fields are in `field_expression_dict` Example usage:: sort_expression = 'likes,-timestamp' field_expression_dict = { 'id': 'map.uid', 'timestamp': 'UNIX_TIMESTAMP(t.date)', 'likes': 'feature.likes' } get_order_by(sort_expression, field_expression_dict) Result:: "ORDER BY likes ASC, timestamp DESC" :param sort_expression: a json-api conform sort expression (see example above) :param valid_fields: a list of valid sort fields :return: an MySQL conform ORDER BY string (see example above) or an empty string if `sort_expression` is None or empty """ if not sort_expression: return '' sort_expressions = sort_expression.split(',') order_bys = [] for expression in sort_expressions: if not expression or expression == '-': continue if expression[0] == '-': order = 'DESC' column = expression[1:] else: order = 'ASC' column = expression if column not in valid_fields: raise ApiException( [Error(ErrorCode.QUERY_INVALID_SORT_FIELD, column)]) order_bys.append('`{}` {}'.format(column, order)) if not order_bys: return '' return 'ORDER BY {}'.format(', '.join(order_bys))
def find_leaderboard_type(rating_type, select=None): rating = {} if rating_type == '1v1': rating['table'] = TABLE1V1 rating['select'] = append_select_expression() rating['tableName'] = 'ladder1v1_rating' elif rating_type == 'global': rating['table'] = TABLEGLOBAL rating['select'] = select rating['tableName'] = 'global_rating' else: raise ApiException( [Error(ErrorCode.QUERY_INVALID_RATING_TYPE, rating_type)]) return rating
def validate_map_info(map_info): errors = [] if not map_info.get('display_name'): errors.append(Error(ErrorCode.MAP_NAME_MISSING)) if not map_info.get('description'): errors.append(Error(ErrorCode.MAP_DESCRIPTION_MISSING)) if not map_info.get('max_players') \ or map_info.get('battle_type') != 'FFA': errors.append(Error(ErrorCode.MAP_FIRST_TEAM_FFA)) if not map_info.get('type'): errors.append(Error(ErrorCode.MAP_TYPE_MISSING)) if not map_info.get('size'): errors.append(Error(ErrorCode.MAP_SIZE_MISSING)) if not map_info.get('version'): errors.append(Error(ErrorCode.MAP_VERSION_MISSING)) if errors: raise ApiException(errors)
def featured_mod_files(id, version): """ Lists the files of a specific version of the specified mod. If the version is "latest", the latest version is returned. **Example Request**: .. sourcecode:: http GET /featured_mods/123/files/3663 **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: text/javascript { "data": [ { "attributes": { "id": "123", "md5": "1bdb6505a6af741509c9d3ed99670b79", "version": "ee2df6c3cb80dc8258428e8fa092bce1", "name": "ForgedAlliance.exe", "group": "bin", "url": "http://content.faforever.com/faf/updaterNew/updates_faf_files/ForgedAlliance.3659.exe" }, "id": "123", "type": "featured_mod_file" }, ... ] } """ mods = get_featured_mods() if id not in mods: raise ApiException([Error(ErrorCode.UNKNOWN_FEATURED_MOD, id)]) featured_mod_name = 'faf' if mods.get(id) == 'ladder1v1' else mods.get(id) files_table = FILES_TABLE_FORMAT.format(featured_mod_name) where = '' args = None if version and version != 'latest': where += ' AND u.version <= %s' args = (version, ) return fetch_data(FeaturedModFileSchema(), files_table, FILES_SELECT_EXPRESSIONS, MAX_PAGE_SIZE, request, enricher=partial( file_enricher, 'updates_{}_files'.format(featured_mod_name)), where_extension=where, args=args)
def process_uploaded_map(temp_map_path, is_ranked): map_info = parse_map_info(temp_map_path, validate=False) validate_map_info(map_info) display_name = map_info['display_name'] version = map_info['version'] description = map_info['description'] max_players = map_info['max_players'] map_type = map_info['type'] battle_type = map_info['battle_type'] size = map_info['size'] width = int(size[0]) height = int(size[1]) if len(display_name) > 100: raise ApiException( [Error(ErrorCode.MAP_NAME_TOO_LONG, 100, len(display_name))]) user_id = request.oauth.user.id if not can_upload_map(display_name, user_id): raise ApiException( [Error(ErrorCode.MAP_NOT_ORIGINAL_AUTHOR, display_name)]) if map_exists(display_name, version): raise ApiException( [Error(ErrorCode.MAP_VERSION_EXISTS, display_name, version)]) zip_file_path = generate_zip(temp_map_path, str(Path(temp_map_path).parent)) zip_file_name = os.path.basename(zip_file_path) target_map_path = os.path.join(app.config['MAP_UPLOAD_PATH'], zip_file_name) if os.path.isfile(target_map_path): raise ApiException([Error(ErrorCode.MAP_NAME_CONFLICT, zip_file_name)]) shutil.move(zip_file_path, target_map_path) generate_map_previews( target_map_path, { 128: os.path.join(app.config['MAP_PREVIEW_PATH'], 'small'), 512: os.path.join(app.config['MAP_PREVIEW_PATH'], 'large') }) with db.connection: cursor = db.connection.cursor(db.pymysql.cursors.DictCursor) cursor.execute( """INSERT INTO map (display_name, map_type, battle_type, author) SELECT %(display_name)s, %(map_type)s, %(battle_type)s, %(author)s WHERE NOT EXISTS ( SELECT display_name FROM map WHERE lower(display_name) = lower(%(display_name)s) ) LIMIT 1""", { 'display_name': display_name, 'map_type': map_type, 'battle_type': battle_type, 'author': user_id }) cursor.execute( """INSERT INTO map_version ( description, max_players, width, height, version, filename, ranked, map_id ) VALUES ( %(description)s, %(max_players)s, %(width)s, %(height)s, %(version)s, %(filename)s, %(ranked)s, (SELECT id FROM map WHERE lower(display_name) = lower(%(display_name)s)) )""", { 'description': description, 'max_players': max_players, 'width': width, 'height': height, 'version': version, 'filename': "maps/" + zip_file_name, 'display_name': display_name, 'ranked': 1 if is_ranked else 0 })
def leaderboards_type(leaderboard_type): """ Lists all ranked 1v1 or global players. **Example Request**: **Default Values**: page[number]=1 page[size]=5000 .. sourcecode:: http GET /leaderboards/1v1 /leaderboards/global Accept: application/vnd.api+json **Example Response**: .. sourcecode:: http HTTP/1.1 200 OK Vary: Accept Content-Type: text/javascript { "data": [ { "attributes": { "deviation": 48.4808, "id": "781", "login": "******", "mean": 2475.69, "num_games": 1285, "ranking": 1, "rating": 2330, "won_games": 946 }, "id": "781", "type": "ranked1v1" }, ... ] } :param page[number]: The page number being requested (EX.: /leaderboards/1v1?page[number]=2) :type page[number]: int :param page[size]: The total amount of players to grab by default (EX.: /leaderboards/1v1?page[size]=10) :type page[size]: int :param leaderboard_type: Finds players in the 1v1 or global rating :type leaderboard_type: 1v1 OR global :status 200: No error """ sort_field = request.values.get('sort') if sort_field: raise ApiException( [Error(ErrorCode.QUERY_INVALID_SORT_FIELD, sort_field)]) page = int(request.values.get('page[number]', 1)) page_size = int(request.values.get('page[size]', MAX_PAGE_SIZE)) row_num = (page - 1) * page_size select = SELECT_EXPRESSIONS args = {'row_num': row_num} rating = find_leaderboard_type(leaderboard_type, select) return fetch_data(LeaderboardSchema(), rating['table'], rating['select'], MAX_PAGE_SIZE, request, sort='-rating', args=args, where='is_active = 1 AND r.numGames > 0')
def process_uploaded_mod(temp_mod_path): mod_info = parse_mod_info(temp_mod_path) validate_mod_info(mod_info) display_name = mod_info['name'] uid = mod_info['uid'] version = mod_info['version'] description = mod_info['description'] author = mod_info['author'] mod_type = 'UI' if mod_info['ui_only'] else 'SIM' user_id = request.oauth.user.id if not can_upload_mod(display_name, user_id): raise ApiException( [Error(ErrorCode.MOD_NOT_ORIGINAL_AUTHOR, display_name)]) if mod_exists(display_name, version): raise ApiException( [Error(ErrorCode.MOD_VERSION_EXISTS, display_name, version)]) zip_file_name = generate_zip_file_name(display_name, version) target_mod_path = os.path.join(app.config['MOD_UPLOAD_PATH'], zip_file_name) if os.path.isfile(target_mod_path): raise ApiException([Error(ErrorCode.MOD_NAME_CONFLICT, zip_file_name)]) thumbnail_path = extract_thumbnail(temp_mod_path, mod_info) shutil.move(temp_mod_path, target_mod_path) with db.connection: cursor = db.connection.cursor(db.pymysql.cursors.DictCursor) cursor.execute( """INSERT INTO `mod` (display_name, author, uploader) SELECT %(display_name)s, %(author)s, %(uploader)s WHERE NOT EXISTS ( SELECT display_name FROM `mod`WHERE lower(display_name) = lower(%(display_name)s) ) LIMIT 1""", { 'display_name': display_name, 'author': author, 'uploader': user_id }) cursor.execute( """INSERT INTO mod_version ( uid, type, description, version, filename, icon, mod_id ) VALUES ( %(uid)s, %(type)s, %(description)s, %(version)s, %(filename)s, %(icon)s, (SELECT id FROM `mod` WHERE lower(display_name) = lower(%(display_name)s)) )""", { 'uid': uid, 'type': mod_type, 'description': description, 'version': version, 'filename': 'mods/' + zip_file_name, 'icon': os.path.basename(thumbnail_path) if thumbnail_path else None, 'display_name': display_name, }) cursor.execute( """INSERT INTO mod_stats (mod_id, likers) SELECT id, '' FROM `mod` WHERE lower(display_name) = lower(%s) AND NOT EXISTS (SELECT mod_id FROM mod_stats WHERE mod_id = id)""", (display_name, ))