aboutsummaryrefslogtreecommitdiff
path: root/api
diff options
context:
space:
mode:
authorlonkaars <l.leblansch@gmail.com>2021-03-28 12:19:28 +0200
committerlonkaars <l.leblansch@gmail.com>2021-03-28 12:19:28 +0200
commit2f4536d6b08b69168ebf3e718cbd8e3002b9af5a (patch)
tree5307692fb341d7f924ee9b73f3751e7e56cfb192 /api
parent1f897d3f5ad11178cf4776ae4070c9d3e832f5f3 (diff)
added comments
Diffstat (limited to 'api')
-rw-r--r--api/app.py1
-rw-r--r--api/auth/login.py6
-rw-r--r--api/auth/login_token.py2
-rw-r--r--api/auth/token.py2
-rw-r--r--api/db.py4
-rw-r--r--api/dynamic_import.py3
-rw-r--r--api/events.py2
-rw-r--r--api/game/accept.py1
-rw-r--r--api/game/cleanup.py2
-rw-r--r--api/game/info.py10
-rw-r--r--api/game/new.py2
-rw-r--r--api/game/random.py3
-rw-r--r--api/game/socket.py1
-rw-r--r--api/game/voerbak_connector.py8
-rw-r--r--api/hierarchy.py1
-rw-r--r--api/log.py3
-rw-r--r--api/main.py1
-rw-r--r--api/passwords.py4
-rw-r--r--api/randid.py1
-rw-r--r--api/rating.py4
-rw-r--r--api/ruleset.py7
-rw-r--r--api/social/create_relation.py4
-rw-r--r--api/social/request_list.py2
-rw-r--r--api/social/search.py3
-rw-r--r--api/socket_io.py1
-rw-r--r--api/test.py12
-rw-r--r--api/user/avatar.py9
-rw-r--r--api/user/games.py3
-rw-r--r--api/user/info.py12
-rw-r--r--api/user/password.py1
-rw-r--r--api/user/preferences.py1
31 files changed, 92 insertions, 24 deletions
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,