summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--COMMANDS.md42
-rw-r--r--README.md22
-rw-r--r--db.py44
-rw-r--r--main.py167
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: