diff options
-rw-r--r-- | COMMANDS.md | 42 | ||||
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | db.py | 44 | ||||
-rw-r--r-- | main.py | 167 |
4 files changed, 247 insertions, 28 deletions
diff --git a/COMMANDS.md b/COMMANDS.md index 8479ac0..273e400 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -4,19 +4,49 @@ Register an account. ### Required fields -- `username`: 1-20 characters, a-z0-9-_ -- `password`: 1-255 characters -- `invite_code`: 16 characters +- string `username`: 1-20 characters, a-z0-9-_ +- string `password`: 1-255 characters +- string `invite_code`: 16 characters ## `login_pswd` Log in using a password. ### Required fields -- `username`: 1-20 characters -- `password`: 1-255 characters +- string `username`: 1-20 characters +- string `password`: 1-255 characters ## `login_token` Log in using a token. ### Required fields -- `token`: 32-127 characters \ No newline at end of file +- string `token`: 32-127 characters + +## `get_user` +Get a user's profile. +*Authentication required.* + +### Required fields +- string `username`: 1-20 characters + +## `set_property` +Set a property of your account, such as your display name or bio. +*Authentication required.* + +### Required fields +- string `property`: 1-63 characters + - Should be a valid property, such as `display_name`, `bio`, `avatar`, or `lastfm`. +- string `value`: 0-2047 characters + +## `get_inbox` +Get the inbox. +*Authentication required.* +*No required fields.* + +## `post` +Create a post. +*Authentication required.* + +### Required fields +- string `content`: 0-3000 characters +- list `replies`: 0-3 items +- list `attachments`: 0-3 items \ No newline at end of file diff --git a/README.md b/README.md index d009856..370437a 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,19 @@ soktdeer rewrite ### general (from hydrogen) - [ ] accounts - [x] account creation - - [ ] get account - - [ ] login via password - - [ ] login via token - - [ ] account editing + - [x] get account + - [x] login via password + - [x] login via token + - [x] account editing + - note: no password changes yet - [ ] account deletion -- [ ] messages - - [ ] message creation - - [ ] get message - - [ ] chat history (v1) -- [ ] get inbox -- [ ] client names +- [x] messages + - [x] message creation + - [x] get message + - [x] chat history (v1) +- [x] get inbox +- [~] client names + - client names will not be implemented in helium ### moderation (from hydrogen) - [ ] bans - [ ] invite codes diff --git a/db.py b/db.py index 88dc79b..6750e47 100644 --- a/db.py +++ b/db.py @@ -94,4 +94,46 @@ class acc: user = usersd.find_one({"username": username}) if not user: return "notExists" - return user["permissions"] \ No newline at end of file + return user["permissions"] + +class posts: + def get_recent(amount=75): + posts = list(postsd.find().sort("created", -1).limit(amount)) + for i in posts: + data = acc.get(i["author"]) + if type(data) != dict: + data = {} + else: + del data["secure"] + del data["profile"] + i["author"] = data + incr = -1 + for j in i["replies"]: + incr += 1 + data = acc.get(j["author"]) + if type(data) != dict: + data = {} + else: + del data["secure"] + del data["profile"] + i["replies"][incr]["author"] = data + return posts + + def get_by_id(post_id): + post = postsd.find_one({"_id": post_id}) + if not post: + return "notExists" + else: + return post + + def add(data): + try: + postsd.insert_one(data) + except Exception as e: + return "fail" + return True + +class inbox: + def get_recent(amount=75): + posts = list(inboxd.find().sort("created", -1).limit(amount)) + return posts \ No newline at end of file diff --git a/main.py b/main.py index a92d4f9..2df74e6 100644 --- a/main.py +++ b/main.py @@ -9,8 +9,10 @@ import db import uuid import secrets import time +from urllib.parse import urlparse version = "Helium-0.0.0a" +attachment_whitelist = ["u.cubeupload.com", "files.catbox.moe", "litter.catbox.moe", "i.ibb.co", "cubeupload.com", "media.tenor.com", "tenor.com", "c.tenor.com", "meower.fraudulent.loan", "fraudulent.loan", "deer.fraudulent.loan"] addr = "localhost" port = 3636 @@ -21,15 +23,26 @@ error_contexts = { "malformedJson": "The JSON data sent to the server could not be parsed.", "lengthInvalid": "A value in the JSON data is longer or shorter than expected.", "invalidUsername": "Username is invalid. It may contain characters that are not permitted in usernames.", + "invalidFormat": "Value contains invalid characters, or is too long.", "invalidInvite": "The invite code you are trying to use is invalid or has expired.", "usernameTaken": "This username has been taken.", "notExists": "The requested value does not exist.", "lockdown": "Maintenance is in progress.", - "authed": "You are already authenticated." + "authed": "You are already authenticated.", + "unauthorized": "You must be authorized to perform this action.", + "deprecated": "This command is no longer supported." +} + +deprecated = { + "set_display_name": "set_property", + "set_avatar": "set_property", + "set_bio": "set_property", + "set_lastfm": "set_property" } ulist = {} -user_clients = {} +client_data = {} + clients = [] invite_codes = ["STANLEYYELNATSAB"] @@ -66,7 +79,7 @@ class util: "command": "greet", "version": version, "ulist": ulist, - "messages": [], + "messages": db.posts.get_recent(), "locked": locked }) @@ -82,12 +95,37 @@ class util: del data["profile"] return data - def authorize(username, conn_id, client=""): + def authorize(username, conn_id, websocket, client=""): ulist[username] = client - user_clients[conn_id] = {"username": username, "client": client} + client_data[conn_id] = {"username": username, "client": client, "websocket": websocket} data = db.acc.get(username) del data["secure"] return data + + def loggedout(username, conn_id, websocket): + del client_data[conn_id] + if username in ulist: + rm_user = True + for i in client_data.copy(): + if client_data[i]["username"] == username: + rm_user = False + if rm_user: + del ulist[username] + broadcast(clients, json.dumps({ + "command": "ulist", + "ulist": ulist + })) + + def forcekick(username): + if username in ulist: + del ulist[username] + for i in client_data.copy(): + if client_data[i]["username"] == username: + try: + client_data[i]["websocket"].close() + except: + pass + del client_data[i] async def handler(websocket): clients.append(websocket) @@ -109,7 +147,7 @@ async def handler(websocket): if fc != True: await websocket.send(util.error(fc, listener)) continue - if str(websocket.id) in user_clients: + if str(websocket.id) in client_data: await websocket.send(util.error("authed", listener)) continue if locked: @@ -161,7 +199,7 @@ async def handler(websocket): if fc != True: await websocket.send(util.error(fc, listener)) continue - if str(websocket.id) in user_clients: + if str(websocket.id) in client_data: await websocket.send(util.error("authed", listener)) continue r["username"] = r["username"].lower() @@ -175,7 +213,7 @@ async def handler(websocket): continue valid = db.acc.verify_pswd(r["username"], r["password"]) if type(valid) == dict: - userdata = util.authorize(r["username"], str(websocket.id)) + userdata = util.authorize(r["username"], str(websocket.id), websocket) await websocket.send(json.dumps({"error": False, "token": valid["token"], "user": userdata, "listener": listener})) util.ulist() continue @@ -190,7 +228,7 @@ async def handler(websocket): if fc != True: await websocket.send(util.error(fc, listener)) continue - if str(websocket.id) in user_clients: + if str(websocket.id) in client_data: await websocket.send(util.error("authed", listener)) continue if locked: @@ -202,19 +240,126 @@ async def handler(websocket): await websocket.send(util.error("banned", listener, db.acc.get_ban(valid["username"]))) continue else: - userdata = util.authorize(valid["username"], str(websocket.id)) + userdata = util.authorize(valid["username"], str(websocket.id), websocket) await websocket.send(json.dumps({"error": False, "user": userdata, "listener": listener})) util.ulist() continue else: await websocket.send(util.error(valid, listener)) continue + elif r["command"] == "get_user": + fc = util.field_check({"username": range(1,21)}, r) + if fc != True: + await websocket.send(util.error(fc, listener)) + continue + if str(websocket.id) not in client_data: + await websocket.send(util.error("unauthorized", listener)) + continue + data = db.acc.get(r["username"]) + if type(data) != dict: + await websocket.send(util.error(data, listener)) + continue + del data["secure"] + await websocket.send(json.dumps({"error": False, "user": data, "listener": listener})) + continue + elif r["command"] == "set_property": + fc = util.field_check({"property": range(1,64), "value": range(0,2048)}, r) + if fc != True: + await websocket.send(util.error(fc, listener)) + continue + if str(websocket.id) not in client_data: + await websocket.send(util.error("unauthorized", listener)) + continue + username = client_data[str(websocket.id)]["username"] + if r["property"] in ["bio", "lastfm"]: + result = db.acc.edit({f"profile.{r['property']}": r["value"]}, username) + if result: + await websocket.send(json.dumps({"error": False, "listener": listener})) + continue + else: + await websocket.send(util.error("fail", listener)) + continue + elif r["property"] == "display_name": + if not re.fullmatch("[a-zA-Z0-9-_,:🅱️. ]{1,20}", r["value"]): + await websocket.send(util.error("invalidFormat", listener)) + continue + result = db.acc.edit({r["property"]: r["value"]}, username) + if result: + await websocket.send(json.dumps({"error": False, "listener": listener})) + continue + else: + await websocket.send(util.error("fail", listener)) + continue + elif r["property"] == "avatar": + if not urlparse(r["value"]).hostname in attachment_whitelist: + await websocket.send(util.error("invalidFormat", listener)) + continue + result = db.acc.edit({r["property"]: r["value"]}, username) + if result: + await websocket.send(json.dumps({"error": False, "listener": listener})) + continue + else: + await websocket.send(util.error("fail", listener)) + continue + else: + await websocket.send(util.error("malformedJson", listener)) + continue + elif r["command"] == "get_inbox": + if str(websocket.id) not in client_data: + await websocket.send(util.error("unauthorized", listener)) + continue + data = db.inbox.get_recent() + await websocket.send(json.dumps({"error": False, "inbox": data, "listener": listener})) + elif r["command"] == "post": + fc = util.field_check({"content": range(0,3001), "replies": range(0,4), "attachments": range(0,4)}, r) + if fc != True: + await websocket.send(util.error(fc, listener)) + continue + if str(websocket.id) not in client_data: + await websocket.send(util.error("unauthorized", listener)) + continue + attachments = [] + for i in r["attachments"]: + if urlparse(i).hostname in attachment_whitelist: + attachments.append(i) + if len(r["content"]) == 0 and len(r["attachments"]) == 0: + await websocket.send(util.error("lengthInvalid", listener)) + continue + username = client_data[str(websocket.id)]["username"] + author = util.author_data(username) + replies = [] + for i in r["replies"]: + post = db.posts.get_by_id(i) + if type(post) == dict: + replies.append(post) + data = { + "_id": str(uuid.uuid4()), + "created": round(time.time()), + "content": r["content"], + "replies": replies, + "attachments": attachments, + "author": username + } + posted = db.posts.add(data) + if posted != True: + await websocket.send(util.error(fc, listener)) + continue + data["author"] = author + broadcast(clients, json.dumps({ + "command": "new_post", + "data": data + })) elif r["command"] == "ping": pass + elif r["command"] in deprecated: + await websocket.send(util.error("deprecated", listener, {"replacement": deprecated[r["command"]]})) + continue else: await websocket.send(util.error("malformedJson", listener)) + if str(websocket.id) in client_data: + util.loggedout(client_data[str(websocket.id)]["username"], str(websocket.id), websocket) if websocket in clients: - clients.append(websocket) + clients.remove(websocket) async def main(): async with serve(handler, addr, port) as server: |