コード例 #1
0
ファイル: helpers.py プロジェクト: ibrewster/api
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
コード例 #2
0
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"
コード例 #3
0
ファイル: helpers.py プロジェクト: ibrewster/api
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
コード例 #4
0
ファイル: maps.py プロジェクト: aeoncleanse/api
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"
コード例 #5
0
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)
コード例 #6
0
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)
コード例 #7
0
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)
コード例 #8
0
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"
コード例 #9
0
ファイル: query_commons.py プロジェクト: ibrewster/api
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
コード例 #10
0
ファイル: helpers.py プロジェクト: ibrewster/api
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
コード例 #11
0
ファイル: query_commons.py プロジェクト: ibrewster/api
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))
コード例 #12
0
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
コード例 #13
0
ファイル: maps.py プロジェクト: aeoncleanse/api
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)
コード例 #14
0
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)
コード例 #15
0
ファイル: maps.py プロジェクト: aeoncleanse/api
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
            })
コード例 #16
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')
コード例 #17
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, ))