From 2f4536d6b08b69168ebf3e718cbd8e3002b9af5a Mon Sep 17 00:00:00 2001 From: lonkaars Date: Sun, 28 Mar 2021 12:19:28 +0200 Subject: added comments --- api/app.py | 1 + api/auth/login.py | 6 ++++++ api/auth/login_token.py | 2 ++ api/auth/token.py | 2 ++ api/db.py | 4 +++- api/dynamic_import.py | 3 +++ api/events.py | 2 ++ api/game/accept.py | 1 + api/game/cleanup.py | 2 ++ api/game/info.py | 10 +++++++++- api/game/new.py | 2 ++ api/game/random.py | 3 +++ api/game/socket.py | 1 + api/game/voerbak_connector.py | 8 ++++++++ api/hierarchy.py | 1 + api/log.py | 3 +++ api/main.py | 1 + api/passwords.py | 4 ++++ api/randid.py | 1 + api/rating.py | 4 ++++ api/ruleset.py | 7 +++++++ api/social/create_relation.py | 4 +++- api/social/request_list.py | 2 ++ api/social/search.py | 3 +++ api/socket_io.py | 1 + api/test.py | 12 ------------ api/user/avatar.py | 9 +++------ api/user/games.py | 3 +++ api/user/info.py | 12 +++++++++--- api/user/password.py | 1 + api/user/preferences.py | 1 + 31 files changed, 92 insertions(+), 24 deletions(-) delete mode 100644 api/test.py (limited to 'api') diff --git a/api/app.py b/api/app.py index 172e820..dfab192 100644 --- a/api/app.py +++ b/api/app.py @@ -1,4 +1,5 @@ from flask import Flask from flask_socketio import SocketIO +# Flask app wrapper (same as db.py) app = Flask(__name__) diff --git a/api/auth/login.py b/api/auth/login.py index 045120a..78a9add 100644 --- a/api/auth/login.py +++ b/api/auth/login.py @@ -9,25 +9,31 @@ login = Blueprint('login', __name__) def index(): data = request.get_json() + # get form data email = data.get("email") or "" password = data.get("password") or "" + # return malformed request if email or password is missing if not email or \ not password: return "", 400 + # resolve user_id from username or email user_id = None user_id = user_id or cursor.execute("select user_id from users where email = ?", [email]).fetchone() user_id = user_id or cursor.execute("select user_id from users where lower(username) = lower(?)", [email]).fetchone() if user_id == None: return "", 401 + # check the password passwd = cursor.execute("select password_hash from users where user_id = ?", [user_id[0]]).fetchone() check = passwords.check_password(password, passwd[0]) if not check: return "", 401 + # generate a new authentication token and add it to the users valid token list new_token = token.generate_token() token.add_token(user_id[0], token.hash_token(new_token)) + # make response with the set_cookie header res = make_response("", 200) res.set_cookie("token", new_token["token"], expires = int(new_token["expirationDate"] / 1000)) diff --git a/api/auth/login_token.py b/api/auth/login_token.py index d5b0e52..d920eea 100644 --- a/api/auth/login_token.py +++ b/api/auth/login_token.py @@ -2,6 +2,7 @@ from flask import Blueprint, request from db import cursor from auth.token import validate_token, hash_token +# get user_id from authentication token def token_login(token): hashed = hash_token({ "token": token, "expirationDate": 0 }) user_id = cursor.execute("select user_id from users where valid_tokens like ?", [f"%{hashed['token']}%"]).fetchone() @@ -9,6 +10,7 @@ def token_login(token): token = Blueprint('token', __name__) +# this endpoint is currently unused, but verifies that a token is valid @token.route('/token', methods = ['POST']) def index(): data = request.get_json() diff --git a/api/auth/token.py b/api/auth/token.py index d2eea52..113c2c7 100644 --- a/api/auth/token.py +++ b/api/auth/token.py @@ -4,8 +4,10 @@ import secrets import json import time +# get valid token hashes for a given user_id def valid_tokens(user_id): tokens = json.loads(cursor.execute("select valid_tokens from users where user_id = ?", [user_id]).fetchone()[0]) + # return only tokens that aren't expired return [token for token in tokens if token["expirationDate"] > int( time.time() * 1000 )] def validate_token(user_id, token): diff --git a/api/db.py b/api/db.py index f5b2539..b850a89 100644 --- a/api/db.py +++ b/api/db.py @@ -1,13 +1,15 @@ -# dit bestand is hier om circular imports te vermijden import os import sqlite3 +# open database db_path = os.path.abspath("database/database.db") connection = sqlite3.connect(db_path, check_same_thread=False) #! ^^^ Dit is onveilig en goede multithreading support moet nog geïmplementeerd worden +# load the sql extension used in social/search connection.enable_load_extension(True) connection.load_extension("./database/levenshtein.sqlext") +# sql cursor cursor = connection.cursor() diff --git a/api/dynamic_import.py b/api/dynamic_import.py index 76281c2..159950c 100644 --- a/api/dynamic_import.py +++ b/api/dynamic_import.py @@ -5,6 +5,7 @@ import os import log import glob +# get all python files in api/ directory and convert them to python import names files = glob.glob(os.path.dirname(__file__) + "/**/*.py", recursive=True) files.remove(__file__) files = [str(filename) @@ -20,8 +21,10 @@ def route(dynamic_route): for file in files: mod = importlib.import_module(file) + # check if module has `dynamic_route` defined (single route) if hasattr(mod, "dynamic_route"): route(mod.dynamic_route) + # check if module has `dynamic_routes` defined (multiple routes as list) elif hasattr(mod, "dynamic_routes"): for dynamic_route in mod.dynamic_routes: route(dynamic_route) diff --git a/api/events.py b/api/events.py index c811be4..ef3ae12 100644 --- a/api/events.py +++ b/api/events.py @@ -7,6 +7,7 @@ from http import cookies from game.cleanup import set_interval import time +# get token from flask_socketio's request.environment def get_token(environ): cookie = environ.get("HTTP_COOKIE") if not cookie: return None @@ -19,6 +20,7 @@ def get_token(environ): return token.value +# global socket connection @io.on("connect") def connect(): token = get_token(request.environ) diff --git a/api/game/accept.py b/api/game/accept.py index 7aab697..073f422 100644 --- a/api/game/accept.py +++ b/api/game/accept.py @@ -12,6 +12,7 @@ from game.new import start_game join_game = Blueprint('game_accept', __name__) +# join a game by game_id (public or private) @join_game.route('/accept', methods = ['POST']) @auth_required("user") def index(game_id): diff --git a/api/game/cleanup.py b/api/game/cleanup.py index 3c285e1..0a9aa46 100644 --- a/api/game/cleanup.py +++ b/api/game/cleanup.py @@ -2,6 +2,7 @@ from db import cursor, connection import threading import time +# cleanup function that's ran every five minutes def cleanup(): now = int( time.time() * 1000 ) old_games = cursor.execute("select game_id from games where (status = \"wait_for_opponent\" or status = \"in_progress\") and last_activity < ?", [now - 5 * 60 * 1e3]).fetchall() @@ -17,5 +18,6 @@ def set_interval(func, sec): # https://stackoverflow.com/questions/2697039/pytho t.start() return t +# run every five minutes set_interval(cleanup, 5 * 60) diff --git a/api/game/info.py b/api/game/info.py index 030c6ae..76a3ef8 100644 --- a/api/game/info.py +++ b/api/game/info.py @@ -22,12 +22,19 @@ def format_game(game_id, user_id = None): "status", # 12 "private", # 13 ]) + " from games where game_id = ?", [game_id]).fetchone() + is_player_1 = game[4] != user_id + + # get opponent from perspective of `user_id` opponent = game[4] if is_player_1 else game[3] + + # parse moves into list and return empty list if moves string is empty + moves = [] if len(game[2]) == 0 else [int(move) for move in str(game[2] + "0").split(",")] + return { "id": game[0], "parent": game[1], - "moves": [] if len(game[2]) == 0 else [int(move) for move in str(game[2] + "0").split(",")], + "moves": moves, "opponent": None if not opponent else format_user(opponent), "outcome": None if not game[5] else outcome(game[5], is_player_1), "created": game[6], @@ -40,6 +47,7 @@ def format_game(game_id, user_id = None): "private": bool(game[13]), } +# check if game_id exists in database def valid_game_id(game_id): query = cursor.execute("select game_id from games where game_id = ?", [game_id]).fetchone() return bool(query) diff --git a/api/game/new.py b/api/game/new.py index 5d8d5b7..9868099 100644 --- a/api/game/new.py +++ b/api/game/new.py @@ -36,6 +36,8 @@ new_game = Blueprint('new_game', __name__) @new_game.route('/new', methods = ["GET", "POST"]) @auth_required("user") def index(user_id): + # create a new private game (join by link) + #TODO: friend invites + notifications game_id = create_game(user_id, True) return { "id": game_id }, 200 diff --git a/api/game/random.py b/api/game/random.py index 7e4c512..4d70b56 100644 --- a/api/game/random.py +++ b/api/game/random.py @@ -14,13 +14,16 @@ random_game = Blueprint('random', __name__) @random_game.route('/random') @auth_required("user") def index(user_id): + # get public_games (random opponent queue) public_games = cursor.execute("select game_id from games where private = FALSE and status = \"wait_for_opponent\"").fetchall() game_started = False + # create a new public game if the queue is empty if len(public_games) == 0: game_id = create_game(user_id) player_1 = True + # otherwise join a random public game else: game_id = random.choice(public_games)[0] diff --git a/api/game/socket.py b/api/game/socket.py index 5f0d710..cf45eb6 100644 --- a/api/game/socket.py +++ b/api/game/socket.py @@ -18,6 +18,7 @@ class game: self.player_1_id = player_1_id self.player_2_id = player_2_id + # drop a disc in `column` def move(self, user_id, column): if user_id != self.player_1_id and user_id != self.player_2_id: return move = self.player_1_id if self.board.player_1 else self.player_2_id diff --git a/api/game/voerbak_connector.py b/api/game/voerbak_connector.py index 2e90ad4..048a5d1 100644 --- a/api/game/voerbak_connector.py +++ b/api/game/voerbak_connector.py @@ -25,6 +25,7 @@ class bord: stderr=None) self.process.stdin.flush() + # get output from voerbak without trailing newline character (this might break on windows because crlf) def get_output(self): return self.process.stdout.readline().decode()[:-1] @@ -32,24 +33,30 @@ class bord: self.process.stdin.write(bytearray("0", "utf-8")) self.process.stdin.flush() + # read messages from voerbak def update_board(self): buffer = self.get_output() while not buffer.isdigit(): + # win message if buffer.startswith("w:"): self.win_positions.append(buffer[2:].split("-")) log.info(f"won: {buffer[2:].split('-')}") self.kill_voerbak() + # error message elif buffer.startswith("e:"): log.warning(buffer[2:]) + # turn update message elif buffer.startswith("m:"): substr = buffer[2:] self.player_1 = True if substr == "true" else False + # draw game message elif buffer.startswith("d:"): self.board_full = True self.kill_voerbak() buffer = self.get_output() self.board = buffer + # debug board print function def print(self): for y in range(self.height -1, -1, -1): for x in range(self.width): @@ -66,6 +73,7 @@ class bord: self.process.stdin.flush() self.update_board() +# debug game def main(): gert = bord(7, 6) while True: diff --git a/api/hierarchy.py b/api/hierarchy.py index 6c1f0af..6844fe6 100644 --- a/api/hierarchy.py +++ b/api/hierarchy.py @@ -4,6 +4,7 @@ from db import cursor ranks = ["none", "user", "moderator", "admin", "bot"] +# @auth_required function decorator (use after @flask.Blueprint.route() decorator) def auth_required(level): def decorator(func): def wrapper(): diff --git a/api/log.py b/api/log.py index 81346ed..5b573b0 100644 --- a/api/log.py +++ b/api/log.py @@ -1,8 +1,11 @@ import logging +# logging module wrapper (same as db.py) VERBOSE = logging.INFO logging.basicConfig(format="[ %(levelname)s ]: %(message)s", level=VERBOSE) +# log functions error = logging.error info = logging.info warning = logging.warning + diff --git a/api/main.py b/api/main.py index 2d07f4c..a35c856 100644 --- a/api/main.py +++ b/api/main.py @@ -4,6 +4,7 @@ from app import app from socket_io import io import game.socket +# start the flask/socket.io server if __name__ == "__main__": io.run(app, host="127.0.0.1", port=5000, debug=True) diff --git a/api/passwords.py b/api/passwords.py index e2ae552..8fa15c3 100644 --- a/api/passwords.py +++ b/api/passwords.py @@ -1,10 +1,14 @@ import bcrypt +# encode string as utf-8 def enc(string): return string.encode('utf-8') +# check if password matches against hash in database def check_password(password, password_hash): return bcrypt.checkpw(enc(password), password_hash) +# hash a password for storing in the database def password_hash(password): return bcrypt.hashpw(enc(password), bcrypt.gensalt()) + diff --git a/api/randid.py b/api/randid.py index 837b0a1..0683c75 100644 --- a/api/randid.py +++ b/api/randid.py @@ -6,6 +6,7 @@ tables = { "games": "game_id" } +# generate a new uuid and check for collisions (unlikely but still) def new_uuid(table_name): temp_uuid = str(uuid.uuid4()) column_name = tables[table_name] diff --git a/api/rating.py b/api/rating.py index e05645f..bc49fda 100644 --- a/api/rating.py +++ b/api/rating.py @@ -14,13 +14,17 @@ def get_all_games(user_id): "where (player_1_id = ? or player_2_id = ?) " + \ "and status = \"finished\" or status = \"resign\"", [user_id, user_id]).fetchall() +# simple rating function that doesn't use game analysis def get_rating(user_id): score = 400 games = get_all_games(user_id) + # get all games for user_id and switch perspective in which user_id is player_2_id mapped_games = [game if game[0] == user_id else (game[1], game[0], outcome(game[2], False)) for game in games] counted_opponents = {} for game in mapped_games: + # calculate sum score against user (+1 for win, -1 for lose, 0 for draw game) counted_opponents |= {game[1]: (counted_opponents.get(game[1]) or 0) + { "w": 1, "l": -1, "d": 0 }[game[2]]} for opponent in counted_opponents: + # apply the cool curve to the sum score and add to the base score of 400 score += rating_v1(counted_opponents.get(opponent)) return int(score) diff --git a/api/ruleset.py b/api/ruleset.py index 3dc59f2..b42b4e9 100644 --- a/api/ruleset.py +++ b/api/ruleset.py @@ -1,6 +1,7 @@ from mergedeep import merge import json +# predefined rulesets rulesets = { "default": { "timelimit": { @@ -14,14 +15,20 @@ rulesets = { } } +# resolve ruleset from ruleset name or dict def resolve_ruleset(ruleset): + # create return variable export = {} try: + # try to parse the ruleset as json export = json.loads(ruleset) merged = dict(rulesets["default"]) + + # fill missing keys in dict merge(merged, export) export = merged except ValueError as e: + # if the ruleset is a name like 'default' or 'columns+2', read it from the predefined rulesets if ruleset in rulesets: export = rulesets[ruleset] if not export: diff --git a/api/social/create_relation.py b/api/social/create_relation.py index eb38978..7e7c466 100644 --- a/api/social/create_relation.py +++ b/api/social/create_relation.py @@ -4,6 +4,8 @@ from hierarchy import auth_required from socket_io import io import time +# @two_person_endpoint decorator +# defines (user_1_id, user_2_id) in endpoint handler function arguments def two_person_endpoint(func): @auth_required("user") def wrapper(user_1_id): @@ -26,6 +28,7 @@ def create_relation(user_1_id, user_2_id, relation_type): [user_1_id, user_2_id, relation_type, timestamp]) connection.commit() +# remove relation between user_1_id and user_2_id (one-way) def remove_relation(user_1_id, user_2_id): cursor.execute("delete from social where user_1_id = ? and user_2_id = ?", [user_1_id, user_2_id]) @@ -42,7 +45,6 @@ def create_relation_route(relation_type): return "", 200 return route - friend_request = Blueprint('friend_request', __name__) friend_request.add_url_rule('/request', 'route', create_relation_route("outgoing"), methods = ["POST"]) diff --git a/api/social/request_list.py b/api/social/request_list.py index a2b23d5..624b3b4 100644 --- a/api/social/request_list.py +++ b/api/social/request_list.py @@ -9,9 +9,11 @@ requests = Blueprint('requests', __name__) @requests.route("/requests") @auth_required("user") def route(user_2_id): + # get a list of friend requests request_list = cursor.execute("select user_1_id from social where user_2_id = ? and type = \"outgoing\"", [user_2_id]).fetchall() + # get user_id for each result to prevent repeat user/info requests formatted_request_list = [] for user_1_id in [q[0] for q in request_list]: formatted_request_list.append(format_user(user_1_id)) diff --git a/api/social/search.py b/api/social/search.py index ccc62e5..1159c67 100644 --- a/api/social/search.py +++ b/api/social/search.py @@ -13,10 +13,13 @@ def index(): if not query: return "", 400 if len(query) < 3: return "", 403 + # use levenshtein with max distance 3 to search for users + #TODO: use mysql and sort by best match results = cursor.execute("select user_id from users where levenshtein(lower(username), lower(?), 3)", [query]).fetchmany(20); formatted = { "results": [] } + # get user_id for each result to prevent repeat user/info requests for user in results: formatted["results"].append(format_user(user[0])) diff --git a/api/socket_io.py b/api/socket_io.py index b80e12e..de3dd9e 100644 --- a/api/socket_io.py +++ b/api/socket_io.py @@ -1,5 +1,6 @@ from flask_socketio import SocketIO from app import app +# socket.io wrapper to avoid circular imports (same as db.py) io = SocketIO(app, cors_allowed_origins="*") diff --git a/api/test.py b/api/test.py deleted file mode 100644 index ba62f00..0000000 --- a/api/test.py +++ /dev/null @@ -1,12 +0,0 @@ -from flask import Blueprint -from hierarchy import auth_required - -test = Blueprint('test_endpoint', __name__) - -@test.route('/test') -@auth_required("user") -def index(): - return "Hello World!" - -dynamic_route = ["/", test] - diff --git a/api/user/avatar.py b/api/user/avatar.py index d3c86b8..b4edeed 100644 --- a/api/user/avatar.py +++ b/api/user/avatar.py @@ -2,6 +2,7 @@ from flask import Blueprint, request, Response from db import cursor from auth.login_token import token_login from user.info import valid_user_id +from hierarchy import auth_required from os.path import exists from codecs import decode @@ -25,14 +26,10 @@ def get_avatar(): return Response(avatar or default_avatar, 200, mimetype="image/png") @avatar.route('/avatar', methods = ["POST"]) #TODO: pillow image size validation (client side resize) -def update_avatar(): - token = request.cookies.get("token") or "" - if not token: return "", 401 +@auth_required("user") +def update_avatar(login): if not request.data: return "", 400 - login = token_login(token) or "" - if not login: return "", 403 - open(f"database/avatars/{login}.png", "wb").write(decode(request.data, "base64")) return "", 200 diff --git a/api/user/games.py b/api/user/games.py index 6072afa..3936566 100644 --- a/api/user/games.py +++ b/api/user/games.py @@ -8,6 +8,7 @@ from ruleset import resolve_ruleset from game.info import format_game import json +# get total game outcome amount for user def sum_games(user_id): #! SANITIZE USER_ID FIRST wld_querys = [' '.join([ "select count(game_id)", @@ -28,6 +29,7 @@ def sum_games(user_id): #! SANITIZE USER_ID FIRST results = cursor.execute(big_query).fetchone() + # win and lose are calculated from user_id's perspective (player_1_id, player_2_id in db) return { "draw": results[0], "win": results[1] + results[4], @@ -35,6 +37,7 @@ def sum_games(user_id): #! SANITIZE USER_ID FIRST "games": reduce(lambda a, b: a + b, results) } +# get `count` games that `user_id` participated in, sorted by newest game def fetch_games(user_id, count): game_ids = cursor.execute("select game_id from games where player_1_id = ? or player_2_id = ? order by created desc", [user_id, user_id]).fetchmany(count) export = [] diff --git a/api/user/info.py b/api/user/info.py index 9a48f4d..be48ef1 100644 --- a/api/user/info.py +++ b/api/user/info.py @@ -4,10 +4,12 @@ from auth.login_token import token_login from rating import get_rating import json +# check if user_id exists in database def valid_user_id(user_id): query = cursor.execute("select user_id from users where user_id = ?", [user_id]).fetchone() return bool(query) +# get relation to user_2_id from user_1_id's perspective def get_relation_to(user_1_id, user_2_id): relation = cursor.execute("select * from social where " + \ "(user_1_id = ? and user_2_id = ?) or " + \ @@ -19,10 +21,12 @@ def get_relation_to(user_1_id, user_2_id): if relation[2] == "block" and relation[0] == user_1_id: return "blocked" return "none" +# get users friend count def count_friends(user_id): query = cursor.execute("select type from social where (user_1_id = ? or user_2_id = ?) and type = \"friendship\"", [user_id, user_id]).fetchall() - return len(query) + return len(query) #FIXME: use SQL count() instead of python's len() +# get user/info of `user_id` as `viewer` (id) def format_user(user_id, viewer = ''): user = cursor.execute("select " + ", ".join([ "username", @@ -38,14 +42,17 @@ def format_user(user_id, viewer = ''): "registered": user[3], "status": user[4], "friends": count_friends(user_id), - "rating": get_rating(user_id), + "rating": get_rating(user_id), #TODO: calculate rating based on game analysis } if viewer: + #FIXME: validate viewer id? formatted_user["relation"] = get_relation_to(viewer, user_id) return formatted_user info = Blueprint('info', __name__) +# view own user/info if no user_id or username is provided and is logged in, +# else view user/info of user with user_id = `user_id` or username = `username` @info.route('/info', methods = ['GET', 'POST']) def index(): data_string = request.data or "{}" @@ -75,7 +82,6 @@ def index(): if user_id and not valid_user_id(user_id): return "", 403 user = format_user(user_id, viewer) - #TODO: rating uitrekenen zodra er game functionaliteit is return user, 200 dynamic_route = ["/user", info] diff --git a/api/user/password.py b/api/user/password.py index 672eda4..0c1cb70 100644 --- a/api/user/password.py +++ b/api/user/password.py @@ -3,6 +3,7 @@ from db import cursor password = Blueprint('password', __name__) +# this endpoint is unfinished @password.route('/password') def index(): data = request.get_json() diff --git a/api/user/preferences.py b/api/user/preferences.py index 9791bfe..d4e27c9 100644 --- a/api/user/preferences.py +++ b/api/user/preferences.py @@ -4,6 +4,7 @@ from ruleset import resolve_ruleset from hierarchy import auth_required import json +# fill missing dict keys in preferences object def format_preferences(prefs): return { "darkMode": prefs.get("darkMode") or False, -- cgit v1.2.3