diff --git a/.env b/.env deleted file mode 100644 index daeef77..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -IPHUB_KEY= \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..658c9ff --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# Set this to true in a development environment +DEVELOPMENT=false + +# Change this if you have a reverse proxy or if it uses a different header +IP_HEADER= + +# To allow for getting IP details and proxy blocking, set up an IPHub account and provide the API key below +IPHUB_KEY= + +# Change this to true if you want to block proxies (requires IPHub to be setup) +BLOCK_PROXIES=false + +# Set this to the host you want Meower server served on +HOST=127.0.0.1 + +# Use these to change the ports Meower services are run on +API_PORT=3000 +CL3_PORT=3001 + +# If needed, change the MongoDB and Redis connection info +DB_URI=mongodb://127.0.0.1:27017 +DB_NAME=meowerserver +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_PASSWORD= diff --git a/.gitignore b/.gitignore index c18dd8d..c61f6af 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__/ +.env diff --git a/.gitmodules b/.gitmodules index 7d30134..38e9537 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "better_profanity"] - path = better_profanity + path = src/common/util/better_profanity url = https://github.com/meower-media-co/better_profanity.git diff --git a/background_worker.py b/background_worker.py new file mode 100644 index 0000000..fff5b76 --- /dev/null +++ b/background_worker.py @@ -0,0 +1,25 @@ +import time + +from src.common.entities import users +from src.common.util import config, display_startup, logging +from src.common.database import db + + +def scheduled_account_deletions(): + _users = db.users.find({"delete_after": {"$lt": int(time.time())}}) + for user in _users: + users.User(**user).delete() + + +if __name__ == "__main__": + display_startup() + + try: + while True: + time.sleep(60) + for task in [scheduled_account_deletions]: + if config.development: + logging.info(f"Running task {task.__name__}...") + task() + except KeyboardInterrupt: + exit() diff --git a/better_profanity b/better_profanity deleted file mode 160000 index a023c27..0000000 --- a/better_profanity +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a023c27ac52d484e2fb166a93a2cd608d9557757 diff --git a/cloudlink.py b/cloudlink.py deleted file mode 100644 index 52a7069..0000000 --- a/cloudlink.py +++ /dev/null @@ -1,1285 +0,0 @@ -#!/usr/bin/env python3 - -version = "0.1.7.6" - -# Server based on https://github.com/Pithikos/python-websocket-server -# Client based on https://github.com/websocket-client/websocket-client - -""" -CloudLink by MikeDEV -Please see https://github.com/MikeDev101/cloudlink for more details. -""" - -import json -import sys -import threading -from websocket_server import WebsocketServer as ws_server -import websocket as ws_client -import time -import traceback -import sys - -""" -Code formatting - -(Type):(Code) | (Description) - -Type: Letter - I - Info - E - Error - -Code: Number, defines the code - -Description: String, Describes the code -""" - -def full_stack(): - exc = sys.exc_info()[0] - if exc is not None: - f = sys.exc_info()[-1].tb_frame.f_back - stack = traceback.extract_stack(f) - else: - stack = traceback.extract_stack()[:-1] - trc = 'Traceback (most recent call last):\n' - stackstr = trc + ''.join(traceback.format_list(stack)) - if exc is not None: - stackstr += ' ' + traceback.format_exc().lstrip(trc) - return stackstr - -class API: - def server(self, ip="127.0.0.1", port=3000, threaded=False): # Runs CloudLink in server mode. - try: - if self.state == 0: - - # Change the link state to 1 (Server mode) - self.state = 1 - self.wss = ws_server( - host=ip, - port=port - ) - - # Set the server's callbacks to CloudLink's class functions - self.wss.set_fn_new_client(self._on_connection_server) - self.wss.set_fn_client_left(self._closed_connection_server) - self.wss.set_fn_message_received(self._on_packet_server) - - # Format dict for storing this mode's specific data - - if (not "motd" in self.statedata) or (not "motd_enable" in self.statedata): - self.statedata["motd_enable"] = False - self.statedata["motd"] = "" - if (not "secure_enable" in self.statedata) or (not "secure_keys" in self.statedata): - self.statedata["secure_enable"] = False - self.statedata["secure_keys"] = [] - if not "ip_blocklist" in self.statedata: - self.statedata["ip_blocklist"] = [""] - - self.statedata = { - "ulist": { - "usernames": {}, - "objs": {} - }, # Username list for the "Usernames" block - "secure_enable": False, # Trusted Access enabler - "secure_keys": [], # Trusted Access keys - "gmsg": "", # Global data stream - "motd_enable": self.statedata["motd_enable"], # MOTD enabler - "motd": self.statedata["motd"], # MOTD text - "secure_enable": self.statedata["secure_enable"], # Trusted Access enabler - "secure_keys": self.statedata["secure_keys"], # Trusted Access keys - "trusted": [], # Clients that are trusted with Secure Access, references memory objects only - "ip_blocklist": self.statedata["ip_blocklist"] # Blocks clients with certain IP addresses - } - - # Run the server - print("Running server on ws://{0}:{1}/".format(ip, port)) - self.wss.run_forever(threaded=threaded) - else: - if self.debug: - print("Error: Attempted to switch states!") - except Exception as e: - if self.debug: - print("Error at client: {0}".format(e)) - - def client(self, ip="ws://127.0.0.1:3000/"): # Runs CloudLink in client mode. - try: - if self.state == 0: - # Change the link state to 2 (Client mode) - self.state = 2 - self.wss = ws_client.WebSocketApp( - ip, - on_message = self._on_packet_client, - on_error = self._on_error_client, - on_open = self._on_connection_client, - on_close = self._closed_connection_client - ) - - # Format dict for storing this mode's specific data - self.statedata = { - "ulist": { - "usernames": [] - }, - } - - # Run the client - self.wss.run_forever() - else: - if self.debug: - print("Error: Attempted to switch states!") - except Exception as e: - if self.debug: - print("Error at client: {0}".format(e)) - - def stop(self, abrupt=False): # Stops CloudLink (not sure if working) - try: - if self.state == 1: - if abrupt: - self.wss.shutdown_abruptly() - else: - self.wss.shutdown_gracefully() - self.state = 0 - elif self.state == 2: - self.wss.close() - else: - if self.debug: - print("Error: Attempted to stop in an invalid mode") - except Exception as e: - if self.debug: - print("Error at stop: {0}".format(e)) - - def callback(self, callback_id, function): # Add user-friendly callbacks for CloudLink to be useful as a module - try: - if callback_id in self.callback_function: - self.callback_function[callback_id] = function - if self.debug: - print("Binded callback {0}.".format(callback_id)) - else: - if self.debug: - print("Error: Callback {0} is not a valid callback id!".format(callback_id)) - except Exception as e: - if self.debug: - print("Error at callback: {0}".format(e)) - - def trustedAccess(self, enable, keys): # Enables secure access to the server. - if type(enable) == bool: - if type(keys) == list: - if enable: - if self.debug: - print("Enabled Trusted Access.") - else: - if self.debug: - print("Disabled Trusted Access.") - self.statedata["secure_enable"] = enable - self.statedata["secure_keys"] = keys - else: - if self.debug: - print('Error: Cannot set Trusted Access keys: expecting , got {0}'.format(type(enable))) - else: - if self.debug: - print('Error: Cannot set Trusted Access enable: expecting , got {0}'.format(type(enable))) - - def sendPacket(self, msg): # User-friendly message sender for both server and client. - try: - if self.state == 1: - if ("id" in msg) and (type(msg["id"]) == dict): # Server is probably passing along the memory object for reference - if self.debug: - print("Info on sendPacket: Server passed along memory object:", msg["id"]["id"], "will try to send packet directly") - try: - client = msg["id"] - del msg["id"] - if self.debug: - print('Sending {0} to {1}'.format(msg, client["id"])) - if self._get_client_type(client) == "scratch": - if ("val" in msg) and (type(msg["val"]) == dict): - msg["val"] = json.dumps(msg["val"]) - self.wss.send_message(client, json.dumps(msg)) - except Exception as e: - if self.debug: - print("Error on sendPacket (server): {0}".format(full_stack())) - - elif ("id" in msg) and (type(msg["id"]) == str) and (msg["cmd"] not in ["gmsg", "gvar"]): - id = msg["id"] - del msg["id"] - if id in self.statedata["ulist"]["usernames"]: - try: - client = self.statedata["ulist"]["objs"][self.statedata["ulist"]["usernames"][id]]["object"] - if self.debug: - print('Sending {0} to {1}'.format(msg, id)) - if self._get_client_type(client) == "scratch": - if ("val" in msg) and (type(msg["val"]) == dict): - msg["val"] = json.dumps(msg["val"]) - self.wss.send_message(client, json.dumps(msg)) - except Exception as e: - if self.debug: - print("Error on sendPacket (server): {0}".format(e)) - else: - try: - if self.debug: - print('Sending "{0}" to all clients'.format(json.dumps(msg))) - self._send_to_all(msg) - except Exception as e: - if self.debug: - print("Error on sendPacket (server): {0}".format(e)) - elif self.state == 2: - try: - if self.debug: - print('Sending {0}'.format(json.dumps(msg))) - self.wss.send(json.dumps(msg)) - except Exception as e: - if self.debug: - print("Error on sendPacket (client): {0}".format(e)) - else: - print("Error: Cannot use the packet sender in current state!") - except Exception as e: - print("Error at sendPacket: {0}".format(e)) - - def setMOTD(self, motd, enable=True): # Sets the MOTD on the server-side. - try: - if type(enable) == bool: - if type(motd) == str: - if enable: - print('Set MOTD to "{0}".'.format(motd)) - self.statedata["motd"] = str(motd) - self.statedata["motd_enable"] = True - else: - print("Disabled MOTD.") - self.statedata["motd"] = None - self.statedata["motd_enable"] = False - else: - print('Error: Cannot set MOTD text: expecting , got {0}'.format(type(enable))) - else: - print('Error: Cannot set the enabler for MOTD: expecting , got {0}'.format(type(enable))) - except Exception as e: - if self.debug: - print("Error at setMOTD: {0}".format(e)) - - def getUsernames(self): # Returns the username list. - if self.state == 1: - return list((self.statedata["ulist"]["usernames"]).keys()) - elif self.state == 2: - return self.statedata["ulist"]["usernames"] - else: - return None - - def getIPofUsername(self, user): # Allows the server to track user IPs for Trusted Access, uses the username of a client. - if self.state == 1: - if not self._get_obj_of_username(user) == None: - return self._get_ip_of_obj(self._get_obj_of_username(user)) - else: - if self.debug: - print("Error: Cannot use the IP getter in current state!") - return "" - - def getIPofObject(self, obj): # Allows the server to track user IPs for Trusted Access, but uses the memory object of a client instead. - if self.state == 1: - return self._get_ip_of_obj(obj) - else: - if self.debug: - print("Error: Cannot use the IP getter in current state!") - return "" - - def untrust(self, obj): # If a client has been trusted, the server can loose trust and refuse future packets. - if self.state == 1: - if self.statedata["secure_enable"]: - if type(obj) == dict: - if obj in self.statedata["trusted"]: - self.statedata["trusted"].remove(obj) - if self.debug: - print("Untrusted ID {0}.".format(obj["id"])) - else: - if self.debug: - print("Unable to untrust an ID that does not exist") - elif type(obj) == str: - obj = self._get_obj_of_username(obj) - if not obj == None: - if obj in self.statedata["trusted"]: - self.statedata["trusted"].remove(obj) - if self.debug: - print("Untrusted ID {0}.".format(obj["id"])) - else: - if self.debug: - print("Unable to untrust an ID that does not exist") - else: - if self.debug: - print("Unable to untrust an ID that does not exist") - else: - if self.debug: - print("Error: Cannot use the untrust function: Trusted Access not enabled!") - else: - if self.debug: - print("Error: Cannot use the untrust function in current state!") - - def loadIPBlocklist(self, blist): # Loads a list of IP addresses to block - if type(blist) == list: - if not '' in blist: - blist.append("") - self.statedata["ip_blocklist"] = blist - if self.debug: - print("Loaded {0} blocked IPs into the blocklist!".format(len(self.statedata["ip_blocklist"])-1)) - - def blockIP(self, ip): # Blocks an IP address - if self.state == 1: - if self.statedata["secure_enable"]: - if type(ip) == str: - if not ip in self.statedata["ip_blocklist"]: - self.statedata["ip_blocklist"].append(ip) - if self.debug: - print("Blocked IP {0}!".format(ip)) - else: - if self.debug: - print("Error: Cannot use the IP Block function in current state!") - - def unblockIP(self, ip): # Unblocks an IP address - if self.state == 1: - if self.statedata["secure_enable"]: - if type(ip) == str: - if ip in self.statedata["ip_blocklist"]: - self.statedata["ip_blocklist"].remove(ip) - if self.debug: - print("Unblocked IP {0}!".format(ip)) - else: - if self.debug: - print("Error: Cannot use the IP Block function in current state!") - - def getIPBlocklist(self): # Returns the latest IP blocklist - if self.state == 1: - if self.statedata["secure_enable"]: - tmp = self.statedata["ip_blocklist"] - tmp.remove('') - return self.statedata["ip_blocklist"] - else: - if self.debug: - print("Error: Cannot use the IP Blocklist get function in current state!") - return [] - - def kickClient(self, obj): # Terminates a client's connection (should only be used for specific purposes) - if self.state == 1: - if self.statedata["secure_enable"]: - if type(obj) == dict: - if obj["id"] in self.statedata["ulist"]["objs"]: - # Ask the WebsocketServer to terminate the connection - obj["handler"].send_close(1000, bytes('', encoding='utf-8')) - if self.debug: - print("Kicked ID {0}.".format(obj["id"])) - else: - if self.debug: - print("Unable to kick an ID that does not exist") - elif type(obj) == str: - obj = self._get_obj_of_username(obj) - if not obj == None: - if obj["id"] in self.statedata["ulist"]["objs"]: - # Ask the WebsocketServer to terminate the connection - obj["handler"].send_close(1000, bytes('', encoding='utf-8')) - if self.debug: - print("Kicked ID {0}.".format(obj["id"])) - else: - if self.debug: - print("Unable to kick an ID that does not exist") - else: - if self.debug: - print("Unable to kick an ID that does not exist") - else: - if self.debug: - print("Error: Cannot use the kick function: Trusted Access not enabled!") - else: - if self.debug: - print("Error: Cannot use the kick function in current state!") - -""" -class CLTLS: #Feature NOT YET IMPLEMENTED - def __init__(self): - pass -""" - -class CloudLink(API): - def __init__(self, debug=False): # Initializes CloudLink - self.wss = None # Websocket Object - self.state = 0 # Module state - self.userlist = [] # Stores usernames set on link - self.callback_function = { # For linking external code, use with functions - "on_connect": None, # Handles new connections (server) or when connected to a server (client) - "on_error": None, # Error reporter - "on_packet": None, # Packet handler - "on_close": None # Runs code when disconnected (client) or server stops (server) - } - self.debug = debug # Print back specific data - self.statedata = {} # Place to store other garbage for modes - self.codes = { # Current set of CloudLink status/error self.codes - "Test": "I:000 | Test", # Test code - "OK": "I:100 | OK", # OK code - "Syntax": "E:101 | Syntax", - "Datatype": "E:102 | Datatype", - "IDNotFound": "E:103 | ID not found", - "InternalServerError": "E:104 | Internal", - "Loop": "E:105 | Loop detected", - "RateLimit": "E:106 | Too many requests", - "TooLarge": "E:107 | Packet too large", - "BrokenPipe": "E:108 | Broken pipe", - "EmptyPacket": "E:109 | Empty packet", - "IDConflict": "E:110 | ID conflict", - "IDSet": "E:111 | ID already set", - "TAEnabled": "I:112 | Trusted Access enabled", - "TAInvalid": "E:113 | TA Key invalid", - "TAExpired": "E:114 | TA Key expired", - "Refused": "E:115 | Refused", - "IDRequired": "E:116 | Username required", - "TALostTrust": "E:117 | Trust lost", - "Invalid": "E:118 | Invalid command", - "Blocked": "E:119 | IP Blocked", - "IPRequred": "E:120 | IP Address required", - "TooManyUserNameChanges": "E:121 | Too Many Username Changes", - "Disabled": "E:122 | Command disabled by sysadmin", - } - - print("CloudLink v{0}".format(str(version))) # Report version number - if self.debug: - print("Debug enabled") - - def _is_json(self, data): # Checks if something is JSON - if type(data) == dict: - return True - else: - try: - tmp = json.loads(data) - return True - except Exception as e: - return False - - def _get_client_type(self, client): # Gets client types to help prevent errors - if client["id"] in self.statedata["ulist"]["objs"]: - return self.statedata["ulist"]["objs"][client["id"]]["type"] - else: - return None - - def _get_obj_of_username(self, client): # Helps mitigate packet spoofing - if client in self.statedata["ulist"]["usernames"]: - return self.statedata["ulist"]["objs"][self.statedata["ulist"]["usernames"][client]]["object"] - else: - return None - - def _get_username_of_obj(self, obj): # Returns the username of a client object - if obj["id"] in self.statedata["ulist"]["objs"]: - return self.statedata["ulist"]["objs"][obj["id"]]["username"] - else: - return "" - - def _get_ip_of_obj(self, obj): # Returns the IP address of a client object - if obj["id"] in self.statedata["ulist"]["objs"]: - return self.statedata["ulist"]["objs"][obj["id"]]["ip"] - else: - return "" - - def _is_obj_trusted(self, obj): # Checks if a client is trusted on the link - if self.statedata["secure_enable"]: - return ((obj in self.statedata["trusted"]) and (not self._is_obj_blocked(obj))) - else: - return False - - def _is_obj_blocked(self, obj): # Checks if a client is IP blocked - if self.statedata["secure_enable"]: - return (self._get_ip_of_obj(obj) in self.statedata["ip_blocklist"]) - else: - return False - - def _send_to_all(self, payload): # "Better" (?) send to all function - tmp_payload = payload - for client in self.wss.clients: - #print("sending {0} to {1}".format(payload, client["id"])) - if self._get_client_type(client) == "scratch": - #print("sending to all, {0} is a scratcher".format(client["id"])) - if ("val" in payload) and (type(payload["val"]) == dict): - #print("stringifying nested json") - tmp_payload["val"] = json.dumps(payload["val"]) - if not self.statedata["secure_enable"]: - self.wss.send_message(client, json.dumps(tmp_payload)) - else: - if self._is_obj_trusted(client): - self.wss.send_message(client, json.dumps(tmp_payload)) - else: - if not self.statedata["secure_enable"]: - self.wss.send_message(client, json.dumps(payload)) - else: - if self._is_obj_trusted(client): - self.wss.send_message(client, json.dumps(payload)) - - def _server_packet_handler(self, client, server, message, listener_detected=False, listener_id=""): # The almighty packet handler, single-handedly responsible for over hundreds of lines of code - if not type(client) == type(None): - if not len(str(message)) == 0: - try: - # Parse the JSON into a dict - msg = json.loads(message) - - if ("id" in msg): - if type(msg["id"]) != str: - if (not type(msg["id"]) == dict) or (not type(msg["id"]) == list): - msg["id"] = str(msg["id"]) - else: - if self.debug: - print('Error: Packet "id" datatype invalid: expecting , got {0}'.format(type(msg["cmd"]))) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Datatype"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Datatype"]})) - return - - # Handle the packet - if "cmd" in msg: # Verify that the packet contains the command parameter, which is needed to work. - if type(msg["cmd"]) == str: - if msg["cmd"] in ["gmsg", "pmsg", "setid", "direct", "gvar", "pvar", "ping"]: - if msg["cmd"] == "gmsg": # Handles global messages. - if False: - if "val" in msg: # Verify that the packet contains the required parameters. - if self._get_client_type(client) == "scratch": - if self._is_json(msg["val"]): - msg["val"] = json.loads(msg["val"]) - if True: - if self.debug: - print("message is {0} bytes".format(len(str(msg["val"])))) - self.statedata["gmsg"] = msg["val"] - # Send the packet to all clients. - self._send_to_all({"cmd": "gmsg", "val": msg["val"]}) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"]})) - else: - if self.debug: - print('Error: Packet too large') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"]})) - else: - if self.debug: - print('Error: Packet missing parameters') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Disabled"]})) - - if msg["cmd"] == "pmsg": # Handles private messages. - if ("val" in msg) and ("id" in msg): # Verify that the packet contains the required parameters. - if self._get_client_type(client) == "scratch": - if self._is_json(msg["val"]): - msg["val"] = json.loads(msg["val"]) - if msg["id"] in self.statedata["ulist"]["usernames"]: - if True: - if not client == self._get_obj_of_username(msg["id"]): - try: - otherclient = self._get_obj_of_username(msg["id"]) - if not len(self._get_username_of_obj(client)) == 0: - msg["origin"] = self._get_username_of_obj(client) - if (self._get_client_type(otherclient) == "scratch") and (self._is_json(msg["val"])): - tmp_val = json.dumps(msg["val"]) - else: - tmp_val = msg["val"] - - if self.debug: - print('Sending {0} to {1}'.format(msg, msg["id"])) - del msg["id"] - self.wss.send_message(otherclient, json.dumps({"cmd": "pmsg", "val": tmp_val, "origin": msg["origin"]})) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"]})) - else: - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDRequired"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDRequired"]})) - except Exception as e: - if self.debug: - print("Error on _server_packet_handler: {0}".format(e)) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - else: - if self.debug: - print('Error: Potential packet loop detected, aborting') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Loop"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Loop"]})) - else: - if self.debug: - print('Error: Packet too large') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"]})) - else: - if self.debug: - print('Error: ID Not found') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDNotFound"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDNotFound"]})) - else: - if self.debug: - print('Error: Packet missing parameters') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - - if msg["cmd"] == "setid": # Sets the username of the client. - if False: - if "val" in msg: # Verify that the packet contains the required parameters. - if not len(str(msg["val"])) == 0: - if True: - if type(msg["val"]) == str: - if self.statedata["ulist"]["objs"][client['id']]["username"] == "": - if not msg["val"] in self.statedata["ulist"]["usernames"]: - # Add the username to the list - self.statedata["ulist"]["usernames"][msg["val"]] = client["id"] - # Set the object's username info - self.statedata["ulist"]["objs"][client['id']]["username"] = msg["val"] - - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"]})) - self._send_to_all({"cmd": "ulist", "val": self._get_ulist()}) - if self.debug: - print("User {0} set username: {1}".format(client["id"], msg["val"])) - else: - if self.debug: - print('Error: Refusing to set username because it would cause a conflict') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDConflict"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDConflict"]})) - else: - if self.debug: - print('Error: Refusing to set username because username has already been set') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDSet"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDSet"]})) - else: - if self.debug: - print('Error: Packet "val" datatype invalid: expecting , got {0}'.format(type(msg["cmd"]))) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Datatype"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Datatype"]})) - else: - if self.debug: - print('Error: Packet too large') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"]})) - else: - if self.debug: - print("Error: Packet is empty") - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["EmptyPacket"]})) - else: - if self.debug: - print('Error: Packet missing parameters') - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Disabled"]})) - - if msg["cmd"] == "direct": # Direct packet handler for server. - if self._get_client_type(client) == "scratch": - if ("val" in msg): - if (self._is_json(msg["val"])) and (type(msg["val"]) == str): - try: - msg["val"] = json.loads(msg["val"]) - except json.decoder.JSONDecodeError: - if self.debug: - print("Failed to decode JSON of direct's nested data") - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - return - - if ("val" in msg): - if not self.callback_function["on_packet"] == None: - if "cmd" in msg["val"]: - if msg["val"]["cmd"] == "type": - if "val" in msg["val"]: - if self.statedata["ulist"]["objs"][client["id"]]["type"] == None: # Prevent the client from changing types - self.statedata["ulist"]["objs"][client["id"]]["type"] = msg["val"]["val"] # Set the client type - if self.debug: - if msg["val"]["val"] == "scratch": - print("Client {0} is scratch type".format(client["id"])) - elif msg["val"]["val"] == "py": - print("Client {0} is python type".format(client["id"])) - elif msg["val"]["val"] == "js": - print("Client {0} is js type".format(client["id"])) - else: - print("Client {0} is of unknown client type, claims it's {1}".format(client["id"], (msg["val"]["val"]))) - #self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"]})) - else: - if self.debug: - print('Error: Packet missing parameters') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - elif msg["val"]["cmd"] == "ip": - try: - if "val" in msg["val"]: - if self.statedata["ulist"]["objs"][client["id"]]["ip"] == None: # Prevent the client from changing IP - self.statedata["ulist"]["objs"][client["id"]]["ip"] = msg["val"]["val"] # Set the client's IP - if self.debug: - print("Client {0} reports IP {1}".format(client["id"], self.statedata["ulist"]["objs"][client["id"]]["ip"])) - #self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"]})) - else: - if self.debug: - print('Error: Packet missing parameters') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - except Exception as e: - if self.debug: - print('Error: Failed to set client IP') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - else: - if "val" in msg["val"]: - if len(self._get_username_of_obj(client)) == 0: - origin = client - if self.debug: - print("Handling direct custom command from {0}".format(origin['id'])) - else: - origin = self._get_username_of_obj(client) - if self.debug: - print("Handling direct custom command from {0}".format(origin)) - - if listener_detected: - self.callback_function["on_packet"]({"cmd": msg["val"]["cmd"], "val": msg["val"]["val"], "id": origin, "listener": listener_id}) - else: - self.callback_function["on_packet"]({"cmd": msg["val"]["cmd"], "val": msg["val"]["val"], "id": origin}) - else: - if self.debug: - print('Error: Packet missing parameters') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - else: - if len(self._get_username_of_obj(client)) == 0: - origin = client - if self.debug: - print("Handling direct command from {0}".format(origin['id'])) - else: - origin = self._get_username_of_obj(client) - if self.debug: - print("Handling direct command from {0}".format(origin)) - if listener_detected: - self.callback_function["on_packet"]({"val": msg["val"], "id": origin, "listener": listener_id}) - else: - self.callback_function["on_packet"]({"val": msg["val"], "id": origin}) - else: - if self.debug: - print('Error: Packet missing parameters') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - - if msg["cmd"] == "gvar": # Handles global variables. - if False: - if ("val" in msg) and ("name" in msg): # Verify that the packet contains the required parameters. - if self._get_client_type(client) == "scratch": - if self._is_json(msg["val"]): - msg["val"] = json.loads(msg["val"]) - if not len(str(msg["name"])) > 100: - # Send the packet to all clients. - self._send_to_all({"cmd": "gvar", "val": msg["val"], "name": msg["name"]}) - - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"]})) - else: - if self.debug: - print('Error: Packet too large') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"]})) - else: - if self.debug: - print('Error: Packet missing parameters') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Disabled"]})) - - if msg["cmd"] == "pvar": # Handles private variables. - if ("val" in msg) and ("id" in msg) and ("name" in msg): # Verify that the packet contains the required parameters. - if self._get_client_type(client) == "scratch": - if self._is_json(msg["val"]): - msg["val"] = json.loads(msg["val"]) - if msg["id"] in self.statedata["ulist"]["usernames"]: - if not len(str(msg["name"])) > 1000: - if not client == self._get_obj_of_username(msg["id"]): - try: - otherclient = self._get_obj_of_username(msg["id"]) - if not len(self._get_username_of_obj(client)) == 0: - msg["origin"] = self._get_username_of_obj(client) - if (self._get_client_type(otherclient) == "scratch") and ((self._is_json(msg["val"])) or (type(msg["val"]) == dict)): - tmp_val = json.dumps(msg["val"]) - else: - tmp_val = msg["val"] - if self.debug: - print('Sending {0} to {1}'.format(msg, msg["id"])) - del msg["id"] - self.wss.send_message(otherclient, json.dumps({"cmd": "pvar", "val": tmp_val, "name": msg["name"], "origin": msg["origin"]})) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"]})) - else: - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDRequired"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDRequired"]})) - except Exception as e: - if self.debug: - print("Error on _server_packet_handler: {0}".format(e)) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - else: - if self.debug: - print('Error: Potential packet loop detected, aborting') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Loop"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Loop"]})) - else: - if self.debug: - print('Error: Packet too large') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"]})) - else: - if self.debug: - print('Error: ID Not found') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDNotFound"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDNotFound"]})) - else: - if self.debug: - print('Error: Packet missing parameters') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - - if msg["cmd"] == "ping": - if self.debug: - print("Ping from client {0}".format(client["id"])) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "ping", "val": self.codes["OK"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "ping", "val": self.codes["OK"]})) - - else: # Route the packet using UPL. - if ("val" in msg) and ("id" in msg): # Verify that the packet contains the required parameters. - if self._get_client_type(client) == "scratch": - if self._is_json(msg["val"]): - msg["val"] = json.loads(msg["val"]) - if msg["id"] in self.statedata["ulist"]["usernames"]: - if True: - if not client == self._get_obj_of_username(msg["id"]): - try: - otherclient = self._get_obj_of_username(msg["id"]) - if not len(self._get_username_of_obj(client)) == 0: - msg["origin"] = self._get_username_of_obj(client) - if (self._get_client_type(otherclient) == "scratch") and ((self._is_json(msg["val"])) or (type(msg["val"]) == dict)): - tmp_val = json.dumps(msg["val"]) - else: - tmp_val = msg["val"] - - if self.debug: - print('Routing {0} to {1}'.format(msg, msg["id"])) - del msg["id"] - self.wss.send_message(otherclient, json.dumps(msg)) - - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"]})) - else: - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDRequired"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDRequired"]})) - except Exception as e: - if self.debug: - print("Error on _server_packet_handler: {0}".format(e)) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - else: - if self.debug: - print('Error: Potential packet loop detected, aborting') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Loop"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Loop"]})) - else: - if self.debug: - print('Error: Packet too large') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TooLarge"]})) - else: - if self.debug: - print('Error: ID Not found') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDNotFound"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IDNotFound"]})) - else: - if self.debug: - print('Error: Packet missing parameters') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - else: - if self.debug: - print('Error: Packet "cmd" datatype invalid: expecting , got {0}'.format(type(msg["cmd"]))) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Datatype"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Datatype"]})) - else: - if self.debug: - print('Error: Packet missing "cmd" parameter') - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - except json.decoder.JSONDecodeError: - if self.debug: - print("Error: Failed to parse JSON") - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - except Exception as e: - if self.debug: - print("Error on _server_packet_handler: {0}".format(full_stack())) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - else: - if self.debug: - print("Error: Packet is empty") - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["EmptyPacket"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["EmptyPacket"]})) - - def _get_ulist(self): # Generates username list - tmp_ulist = list((self.statedata["ulist"]["usernames"]).keys()) - for item in tmp_ulist: - if item[0] == "%" and item[len(item)-1] == "%": - tmp_ulist.pop(tmp_ulist.index(item)) - - output = "" - for item in tmp_ulist: - output = output + item + ";" - return output - - def _on_connection_server(self, client, server): # Server-side new connection handler - if not type(client) == type(None): - try: - if self.debug: - print("New connection: {0}".format(str(client['id']))) - - # Add the client to the ulist object in memory. - self.statedata["ulist"]["objs"][client["id"]] = {"object": client, "username": "", "ip": None, "type": None} - - # Send the MOTD if enabled. - if self.statedata["motd_enable"]: - self.wss.send_message(client, json.dumps({"cmd": "direct", "val": {"cmd": "motd", "val": str(self.statedata["motd"])}})) - - # Send server version. - self.wss.send_message(client, json.dumps({"cmd": "direct", "val": {"cmd": "vers", "val": str(version)}})) - - if not self.statedata["secure_enable"]: - # Send the current username list. - self.wss.send_message(client, json.dumps({"cmd": "ulist", "val": self._get_ulist()})) - - # Send the current global data stream value. - self.wss.send_message(client, json.dumps({"cmd": "gmsg", "val": str(self.statedata["gmsg"])})) - else: - # Tell the client that the server is expecting a Trusted Access key. - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TAEnabled"]})) - - if not self.callback_function["on_connect"] == None: - def run(*args): - try: - self.callback_function["on_connect"](client) - except Exception as e: - if self.debug: - print("Error on _on_connection_server: {0}".format(e)) - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - threading.Thread(target=run).start() - except Exception as e: - if self.debug: - print("Error on _on_connection_server: {0}".format(e)) - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - - def _closed_connection_server(self, client, server): # Server-side client closed connection handler - if not type(client) == type(None): - try: - if self.debug: - if self.statedata["ulist"]["objs"][client['id']]["username"] == "": - print("Connection closed: {0}".format(str(client['id']))) - else: - print("Connection closed: {0} ({1})".format(str(client['id']), str(self.statedata["ulist"]["objs"][client['id']]["username"]))) - - if not self.callback_function["on_close"] == None: - try: - self.callback_function["on_close"](client) - except Exception as e: - if self.debug: - print("Error on _closed_connection_server: {0}".format(e)) - - # Remove entries from username list and userlist objects - if self.statedata["ulist"]["objs"][client['id']]["username"] in self.statedata["ulist"]["usernames"]: - del self.statedata["ulist"]["usernames"][self.statedata["ulist"]["objs"][client['id']]["username"]] - del self.statedata["ulist"]["objs"][client['id']] - - if self.statedata["secure_enable"]: - if client in self.statedata["trusted"]: - self.statedata["trusted"].remove(client) - - self._send_to_all({"cmd": "ulist", "val": self._get_ulist()}) - except Exception as e: - if self.debug: - print("Error on _closed_connection_server: {0}".format(e)) - - def _on_packet_server(self, client, server, message): # Server-side new packet handler (Gives it's powers to _server_packet_handler) - if not type(client) == type(None): - try: - if self.debug: - print("New packet from {0}: {1} bytes".format(str(client['id']), str(len(message)))) - if self.statedata["secure_enable"]: - if not self._is_obj_trusted(client): - try: - msg = json.loads(message) - listener_detected = (("listener" in msg) and (type(msg["listener"]) == str)) - listener_id = "" - # Support listener IDs feature from CL Turbo - if listener_detected: - listener_id = msg["listener"] - - if ("cmd" in msg) and ("val" in msg): - if (msg["cmd"] == "direct") and (type(msg["val"]) == dict) and (msg["val"]["cmd"] in ["ip", "type"]): - if self._is_obj_blocked(client): - if self.debug: - print("User {0} is IP blocked, not trusting".format(client["id"])) - # Tell the client it is IP blocked - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Blocked"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Blocked"]})) - else: - self._server_packet_handler(client, server, message, listener_detected, listener_id) - else: - if (msg["cmd"] == "direct") or (msg["cmd"] == "gmsg"): - if self._is_obj_blocked(client): - if self.debug: - print("User {0} is IP blocked, not trusting".format(client["id"])) - # Tell the client it is IP blocked - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Blocked"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Blocked"]})) - else: - if type(msg["val"]) == str: - if msg["val"] in self.statedata["secure_keys"]: - if self._get_ip_of_obj(client) == None: - if self.debug: - print("User {0} has not set their IP address, not trusting".format(client["id"])) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IPRequred"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["IPRequred"]})) - else: - self.statedata["trusted"].append(client) - if self.debug: - print("Trusting user {0}".format(client["id"])) - - # Send the current username list. - self.wss.send_message(client, json.dumps({"cmd": "ulist", "val": self._get_ulist()})) - - # Send the current global data stream value. - self.wss.send_message(client, json.dumps({"cmd": "gmsg", "val": str(self.statedata["gmsg"])})) - - # Tell the client it has been trusted - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["OK"]})) - else: - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TAInvalid"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["TAInvalid"]})) - else: - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Datatype"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Datatype"]})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Refused"]})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - except json.decoder.JSONDecodeError: - if self.debug: - print("Error on _on_packet_server: Failed to parse JSON") - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["Syntax"]})) - else: - try: - msg = json.loads(message) - listener_detected = (("listener" in msg) and (type(msg["listener"]) == str)) - listener_id = "" - # Support listener IDs feature from CL Turbo - if listener_detected: - listener_id = msg["listener"] - except: - listener_detected = False - listener_id = "" - def run(*args): - try: - self._server_packet_handler(client, server, message, listener_detected, listener_id) - except Exception as e: - if self.debug: - print("Error on _on_packet_server: {0}".format(e)) - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - threading.Thread(target=run).start() - else: - def run(*args): - try: - msg = json.loads(message) - listener_detected = (("listener" in msg) and (type(msg["listener"]) == str)) - listener_id = "" - # Support listener IDs feature from CL Turbo - if listener_detected: - listener_id = msg["listener"] - except: - listener_detected = False - listener_id = "" - try: - self._server_packet_handler(client, server, message, listener_detected, listener_id) - except Exception as e: - if self.debug: - print("Error on _on_packet_server: {0}".format(e)) - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - threading.Thread(target=run).start() - except Exception as e: - try: - msg = json.loads(message) - listener_detected = (("listener" in msg) and (type(msg["listener"]) == str)) - listener_id = "" - # Support listener IDs feature from CL Turbo - if listener_detected: - listener_id = msg["listener"] - except: - listener_detected = False - listener_id = "" - if self.debug: - print("Error on _on_packet_server: {0}".format(e)) - if listener_detected: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"], "listener": listener_id})) - else: - self.wss.send_message(client, json.dumps({"cmd": "statuscode", "val": self.codes["InternalServerError"]})) - - def _on_connection_client(self, ws): # Client-side connection handler - try: - if self.debug: - print("Connected") - self.wss.send(json.dumps({"cmd": "direct", "val": {"cmd": "type", "val": "py"}})) # Specify to the server that the client is based on Python - if not self.callback_function["on_connect"] == None: - def run(*args): - try: - self.callback_function["on_connect"]() - except Exception as e: - if self.debug: - print("Error on _on_connection_client: {0}".format(e)) - threading.Thread(target=run).start() - except Exception as e: - if self.debug: - print("Error on _on_connection_client: {0}".format(e)) - - def _on_packet_client(self, ws, message): # Client-side packet handler - try: - if self.debug: - print("New packet: {0}".format(message)) - - tmp = json.loads(message) - if (("cmd" in tmp) and (tmp["cmd"] == "ulist")) and ("val" in tmp): - self.statedata["ulist"]["usernames"] = str(tmp["val"]).split(";") - del self.statedata["ulist"]["usernames"][len(self.statedata["ulist"]["usernames"])-1] - if self.debug: - print("Username list:", str(self.statedata["ulist"]["usernames"])) - - if not self.callback_function["on_packet"] == None: - def run(*args): - try: - self.callback_function["on_packet"](message) - except Exception as e: - if self.debug: - print("Error on _on_packet_client: {0}".format(e)) - threading.Thread(target=run).start() - except Exception as e: - if self.debug: - print("Error on _on_packet_client: {0}".format(e)) - - def _on_error_client(self, ws, error): # Client-side error handler - try: - if self.debug: - print("Error: {0}".format(str(error))) - if not self.callback_function["on_error"] == None: - def run(*args): - try: - self.callback_function["on_error"](error) - except Exception as e: - if self.debug: - print("Error on _on_error_client: {0}".format(e)) - threading.Thread(target=run).start() - except Exception as e: - if self.debug: - print("Error on _on_error_client: {0}".format(e)) - - def _closed_connection_client(self, ws, close_status_code, close_msg): #Client-side closed connection handler - try: - if self.debug: - print("Closed, status: {0} with code {1}".format(str(close_status_code), str(close_msg))) - if not self.callback_function["on_close"] == None: - def run(*args): - try: - self.callback_function["on_close"]() - except Exception as e: - if self.debug: - print("Error on _closed_connection_client: {0}".format(e)) - threading.Thread(target=run).start() - except Exception as e: - if self.debug: - print("Error on _closed_connection_client: {0}".format(e)) diff --git a/convert.py b/convert.py deleted file mode 100644 index 16ecaa6..0000000 --- a/convert.py +++ /dev/null @@ -1,104 +0,0 @@ -from pymongo import MongoClient -import os -import json -from supporter import Supporter - -db = MongoClient("mongodb://localhost:27017")["meowerserver"] -supporter = Supporter() - -username_changes = {} # {"": ""} -password_changes = {} # {"": ""} -- Hashed passwords only -ban = [] -unban = [] -delete = [] - -if input("Welcome to the Meower server converter!\n\nType 'y' and press 'enter' to confirm you want to copy all data from the 'Meower' db folder to MongoDB.").lower() != "y": - input("Aborted! Press 'enter' to exit.") - exit() - -print("Adding config data to Mongo...") -with open("Meower/Config/filter.json", 'r') as f: - filter = json.loads(f.read()) -db["config"].find_one_and_replace({"_id": "filter"}, filter) -print("Added filter to Mongo!") - -with open("Meower/Config/supported_versions.json", 'r') as f: - supported_versions = json.loads(f.read()) -db["config"].find_one_and_replace({"_id": "supported_versions"}, supported_versions) -print("Added supported_versions to Mongo!") - -with open("Meower/Config/trust_keys.json", 'r') as f: - trust_keys = json.loads(f.read()) -db["config"].find_one_and_replace({"_id": "trust_keys"}, trust_keys) -print("Added trust_keys to Mongo!") - -with open("Meower/Jail/IPBanlist.json", 'r') as f: - IPBanlist = json.loads(f.read()) -db["config"].find_one_and_replace({"_id": "IPBanlist"}, IPBanlist) -print("Added IPBanlist to Mongo!") - -print("Adding users to Mongo...") -for item in os.listdir("Meower/Userdata"): - try: - with open("Meower/Userdata/{0}".format(item), 'r') as f: - userdata = json.loads(f.read()) - if item in username_changes: - print("Changing username for {0} to {1}".format(item, username_changes[item])) - userdata["_id"] = username_changes[item] - else: - userdata["_id"] = item - if item in password_changes: - print("Changing password for {0} to {1}".format(item, password_changes[item])) - userdata["pswd"] = password_changes[item] - if item in ban: - print("Banning {0}".format(item)) - userdata["banned"] = True - if item in unban: - print("Unbanning {0}".format(item)) - userdata["banned"] = False - userdata["last_ip"] = "" - db["usersv0"].insert_one(userdata) - print("Added {0} to Mongo!".format(userdata["_id"])) - except Exception as e: - print("Error: {0}".format(e)) - -print("Running integrity checks...") -failed = [] -success = [] -deleted = [] -datatypes = {"theme": str, "mode": bool, "sfx": bool, "debug": bool, "bgm": bool, "bgm_song": int, "layout": str, "pfp_data": int, "quote": str, "email": str, "pswd": str, "lvl": int, "banned": bool, "last_ip": str} -default_values = {"theme": "orange", "mode": True, "sfx": True, "debug": False, "bgm": True, "bgm_song": 2, "layout": "new", "pfp_data": 1, "quote": "", "email": "", "last_ip": ""} -for item in os.listdir("Meower/Userdata"): - userdata = db["usersv0"].find_one({"_id": item}) - if userdata == None: - failed.append(item) - print("Faile to move {0} to Mongo due to blank or severily corrupted userdata file!".format(item)) - else: - if not item in ["Deleted", "Server", "username", "", "Meower"]: - if item in delete: - print("Deleted {0}".format(item)) - db["usersv0"].delete_one({"_id": item}) - deleted.append(item) - for key in datatypes.keys(): - if type(userdata[key]) != datatypes[key]: - if key in default_values: - userdata[key] = default_values[key] - try: - db["usersv0"].find_one_and_replace({"_id": item}, userdata) - print("Restored {0} in {1} to default value due to minor integrity error!".format(key, item)) - except: - failed.append(item) - print("Failed applying fix for {0} at {1}!".format(item, key)) - db["usersv0"].delete_one({"_id": item}) - break - else: - failed.append(item) - print("Failed integrity check for {0} at {1}!".format(item, key)) - db["usersv0"].delete_one({"_id": item}) - break - if not item in failed: - success.append(item) - -print("\n\nConversion complete!\nSucceeded: {0}\nFailed: {1} : {2}\nDeleted: {3} : {4}".format(len(success), len(failed), failed, len(deleted), deleted)) - -input("Press 'enter' to exit.") \ No newline at end of file diff --git a/converter.py b/converter.py deleted file mode 100644 index 6f55947..0000000 --- a/converter.py +++ /dev/null @@ -1,61 +0,0 @@ -import pymongo -import string -from uuid import uuid4 - -permitted_chars_username = [] -delete_these = [] - -def checkForBadCharsUsername(value): - badchars = False - for char in value: - if not char in permitted_chars_username: - badchars = True - break - return badchars - -# Make permitted chars list -for char in string.ascii_letters: - permitted_chars_username.append(char) -for char in string.digits: - permitted_chars_username.append(char) -permitted_chars_username.extend(["_", "-"]) - -# Connect to DB -db = pymongo.MongoClient("mongodb://localhost:27017")["meowerserver"] -print("Connected to database!") - -# Fix up my dumb spelling mistake -db["posts"].delete_many({"p": {"$regex": "Message a moderator"}}) - -# Update users -db["usersv0"].update_many({"unread_inbox": None}, {"$set": {"unread_inbox": True}}) -db["usersv0"].update_many({"created": None}, {"$set": {"created": 1636929928}}) -db["usersv0"].update_many({"tokens": None}, {"$set": {"tokens": []}}) -db["usersv0"].update_many({"last_ip": None}, {"$set": {"last_ip": None}}) -for user in db["usersv0"].find(): - if (len(user["_id"]) > 20) or (checkForBadCharsUsername(user["_id"])) or (len(user["_id"].strip()) == 0): - delete_these.append(user["_id"]) - else: - db["usersv0"].update_one({"_id": user["_id"]}, {"$set": {"lower_username": user["_id"].lower(), "uuid": str(uuid4())}}) - -# Delete bad accounts -if len(delete_these) > 0: - print(delete_these) - confirm = input("Delete {0} bad accounts? (y/n) ".format(len(delete_these))) - if confirm == "y": - for username in delete_these: - db["posts"].delete_many({"u": username}) - chat_index = db["chats"].find({"members": {"$all": [username]}}) - for chat in chat_index: - if chat["owner"] == username: - db["chats"].delete_one({"_id": chat["_id"]}) - else: - chat["members"].remove(username) - db["chats"].update_one({"_id": chat["_id"]}, {"$set": {"members": chat["members"]}}) - netlog_index = db["netlog"].find({"users": {"$all": [username]}}) - for ip in netlog_index: - ip["users"].remove(username) - if ip["last_user"] == username: - ip["last_user"] = "Deleted" - db["netlog"].update_one({"_id": ip["_id"]}, {"$set": {"users": ip["users"], "last_user": ip["last_user"]}}) - db["usersv0"].delete_one({"_id": username}) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b81916e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: "3.1" + +services: + # MongoDB + meower-database: + image: mongo + hostname: mongo + restart: always + volumes: + - ./data/db:/data/db + + # Redis + meower-redis: + image: redis:alpine + hostname: redis + restart: always + + # Meower Server + meower-server: + build: . + env_file: .env + restart: always + depends_on: + - meower-database + - meower-redis + ports: + - 3000:3000 + - 3001:3001 + environment: + - HOST=0.0.0.0 + - DB_URI=mongodb://mongo:27017/ + - REDIS_HOST=redis \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..13406db --- /dev/null +++ b/dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-alpine +WORKDIR /app +COPY . . +RUN apk add --no-cache bash +RUN pip install -r requirements.txt +CMD /bin/bash /app/start_server.sh \ No newline at end of file diff --git a/files.py b/files.py deleted file mode 100644 index d5ae134..0000000 --- a/files.py +++ /dev/null @@ -1,177 +0,0 @@ -from pymongo import MongoClient -import time -from uuid import uuid4 - -""" - -Meower Files Module - -This module provides filesystem functionality and a primitive JSON-file based database interface. -This file should be modified/refactored to interact with a JSON-friendly database server instead of filesystem directories and files. - -""" - -class Files: - def __init__(self, logger, errorhandler): - self.log = logger - self.errorhandler = errorhandler - - mongo_ip = "mongodb://localhost:27017" - self.log("Connecting to database '{0}'\n(If it seems like the server is stuck or the server randomly crashes, it probably means it couldn't connect to the database)".format(mongo_ip)) - self.db = MongoClient(mongo_ip)["meowerserver"] - - # Check connection status - if self.db.client.get_database("meowerserver") == None: - self.log("Failed to connect to MongoDB database!") - else: - self.log("Connected to database") - - # Create database collections - for item in ["config", "usersv0", "usersv1", "netlog", "posts", "chats", "reports"]: - if not item in self.db.list_collection_names(): - self.log("Creating collection {0}".format(item)) - self.db.create_collection(name=item) - - # Create collection indexes - self.db["netlog"].create_index("users") - self.db["usersv0"].create_index("lower_username") - self.db["posts"].create_index("u") - self.db["posts"].create_index("post_origin") - self.db["posts"].create_index("type") - self.db["posts"].create_index("p") - self.db["chats"].create_index("members") - - # Create reserved accounts - for username in ["Server", "Deleted", "Meower", "Admin", "username"]: - self.create_item("usersv0", username, { - "lower_username": username.lower(), - "created": int(time.time()), - "uuid": str(uuid4()), - "unread_inbox": False, - "theme": "", - "mode": None, - "sfx": None, - "debug": None, - "bgm": None, - "bgm_song": None, - "layout": None, - "pfp_data": None, - "quote": None, - "email": None, - "pswd": None, - "tokens": [], - "lvl": None, - "banned": False, - "last_ip": None - }) - - # Create IP banlist file - self.create_item("config", "IPBanlist", { - "wildcard": [], - "users": {} - }) - - # Create Version support file - self.create_item("config", "supported_versions", { - "index": [ - "scratch-beta-5-r7", - ] - }) - - # Create Trust Keys file - self.create_item("config", "trust_keys", { - "index": [ - "meower", - ] - }) - - # Create Filter file - self.create_item("config", "filter", { - "whitelist": [], - "blacklist": [] - }) - - # Create status file - self.create_item("config", "status", { - "repair_mode": False, - "is_deprecated": False - }) - - self.log("Files initialized!") - - def does_item_exist(self, collection, id): - if collection in self.db.list_collection_names(): - if self.db[collection].find_one({"_id": id}) != None: - return True - else: - return False - else: - return False - - def create_item(self, collection, id, data): - if collection in self.db.list_collection_names(): - if not self.does_item_exist(collection, id): - data["_id"] = id - self.db[collection].insert_one(data) - return True - else: - self.log("{0} already exists in {1}".format(id, collection)) - return False - else: - self.log("{0} collection doesn't exist".format(collection)) - return False - - def update_item(self, collection, id, data): - if collection in self.db.list_collection_names(): - if self.does_item_exist(collection, id): - self.db[collection].update_one({"_id": id}, {"$set": data}) - return True - else: - return False - else: - return False - - def write_item(self, collection, id, data): - if collection in self.db.list_collection_names(): - if self.does_item_exist(collection, id): - data["_id"] = id - self.db[collection].find_one_and_replace({"_id": id}, data) - return True - else: - return False - else: - return False - - def load_item(self, collection, id): - if collection in self.db.list_collection_names(): - if self.does_item_exist(collection, id): - return True, self.db[collection].find_one({"_id": id}) - else: - return False, None - else: - return False, None - - def find_items(self, collection, query): - if collection in self.db.list_collection_names(): - payload = [] - for item in self.db[collection].find(query): - payload.append(item["_id"]) - return payload - else: - return [] - - def count_items(self, collection, query): - if collection in self.db.list_collection_names(): - return self.db[collection].count_documents(query) - else: - return 0 - - def delete_item(self, collection, id): - if collection in self.db.list_collection_names(): - if self.does_item_exist(collection, id): - self.db[collection].delete_one({"_id": id}) - return True - else: - return False - else: - return False \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index 7bd1e5d..0000000 --- a/main.py +++ /dev/null @@ -1,136 +0,0 @@ -from cloudlink import CloudLink -from supporter import Supporter -from security import Security -from files import Files -from meower import Meower -from rest_api import app as rest_api_app -from threading import Thread - -""" - -Meower Social Media Platform - Server Source Code - -Dependencies: -* CloudLink >=0.1.7.6 -* better-profanity -* bcrypt -* traceback -* datetime -* os -* sys -* json -* random - -""" - -class Main: - def __init__(self, debug=False): - # Initalize libraries - self.cl = CloudLink(debug=debug) # CloudLink Server - self.supporter = Supporter( # Support functionality - cl = self.cl, - packet_callback = self.handle_packet - ) - self.filesystem = Files( # Filesystem/Database I/O - logger = self.supporter.log, - errorhandler = self.supporter.full_stack - ) - self.accounts = Security( # Security and account management - files = self.filesystem, - supporter = self.supporter, - logger = self.supporter.log, - errorhandler = self.supporter.full_stack - ) - - # Initialize Meower - self.meower = Meower( - supporter = self.supporter, - cl = self.cl, - logger = self.supporter.log, - errorhandler = self.supporter.full_stack, - accounts = self.accounts, - files = self.filesystem - ) - - # Load trust keys - result, payload = self.filesystem.load_item("config", "trust_keys") - if result: - self.cl.trustedAccess(True, payload["index"]) - - # Load IP Banlist - ips = [] - for netlog in self.filesystem.db["netlog"].find({"blocked": True}): - ips.append(netlog["_id"]) - self.cl.loadIPBlocklist(ips) - - # Set server MOTD - self.cl.setMOTD("Meower Social Media Platform Server", True) - - # Run REST API - Thread(target=rest_api_app.run, kwargs={"host": "0.0.0.0", "port": 3001, "debug": False, "use_reloader": False}).start() - - # Run CloudLink server - self.cl.server(port=3000, ip="0.0.0.0") - - def returnCode(self, client, code, listener_detected, listener_id): - self.supporter.sendPacket({"cmd": "statuscode", "val": self.cl.codes[str(code)], "id": client}, listener_detected = listener_detected, listener_id = listener_id) - - def handle_packet(self, cmd, ip, val, listener_detected, listener_id, client, clienttype): - try: - commands = set([ - "ping", - "version_chk", - "get_ulist", - "authpswd", - "gen_account", - "get_profile", - "update_config", - "change_pswd", - "del_tokens", - "del_account", - "get_home", - "get_inbox", - "post_home", - "get_post", - "get_peak_users", - "search_user_posts", - "report", - "close_report", - "clear_home", - "clear_user_posts", - "alert", - "announce", - "block", - "unblock", - "kick", - "get_user_ip", - "get_ip_data", - "get_user_data", - "ban", - "pardon", - "terminate", - "repair_mode", - "delete_post", - "post_chat", - "set_chat_state", - "create_chat", - "leave_chat", - "get_chat_list", - "get_chat_data", - "get_chat_posts", - "add_to_chat", - "remove_from_chat" - ]) - if cmd in commands: - getattr(self.meower, cmd)(client, val, listener_detected, listener_id) - else: - # Catch-all error code - self.returnCode(code = "Invalid", client = client, listener_detected = listener_detected, listener_id = listener_id) - except Exception: - self.supporter.log("{0}".format(self.supporter.full_stack())) - - # Catch-all error code - self.returnCode(code = "InternalServerError", client = client, listener_detected = listener_detected, listener_id = listener_id) - -if __name__ == "__main__": - Main(debug=True) \ No newline at end of file diff --git a/meower.py b/meower.py deleted file mode 100644 index 3aeec5b..0000000 --- a/meower.py +++ /dev/null @@ -1,1800 +0,0 @@ -import time -import uuid -import secrets -import pymongo -import os -from dotenv import load_dotenv -import requests - -load_dotenv() # take environment variables from .env. - -class Meower: - def __init__(self, cl, supporter, logger, errorhandler, accounts, files): - self.cl = cl - self.supporter = supporter - self.log = logger - self.errorhandler = errorhandler - self.accounts = accounts - self.filesystem = files - self.sendPacket = self.supporter.sendPacket - result, self.supporter.filter = self.filesystem.load_item("config", "filter") - if not result: - self.log("Failed to load profanity filter, default will be used as fallback!") - result, self.supporter.status = self.filesystem.load_item("config", "status") - if not result: - self.log("Failed to load status, server will enable repair mode!") - self.supporter.status = {"repair_mode": True, "is_deprecated": False} - self.log("Meower initialized!") - - # Some Meower-library specific utilities needed - - def checkForInt(self, data): - try: - int(data) - return True - except ValueError: - return False - - def getIndex(self, location="posts", query={"post_origin": "home", "isDeleted": False}, truncate=False, page=1, sort="t.e"): - if truncate: - all_items = self.filesystem.db[location].find(query).sort("t.e", pymongo.DESCENDING).skip((page-1)*25).limit(25) - else: - all_items = self.filesystem.db[location].find(query) - - item_count = self.filesystem.db[location].count_documents(query) - if item_count == 0: - pages = 0 - else: - if (item_count % 25) == 0: - if (item_count < 25): - pages = 1 - else: - pages = (item_count // 25) - else: - pages = (item_count // 25)+1 - - query_get = [] - for item in all_items: - query_get.append(item) - - query_return = { - "query": query, - "index": query_get, - "page#": page, - "pages": pages - } - - return query_return - - def createPost(self, post_origin, user, content): - post_id = str(uuid.uuid4()) - timestamp = self.supporter.timestamp(1).copy() - - post_data = { - "type": 1, - "post_origin": str(post_origin), - "u": str(user), - "t": timestamp, - "p": str(content), - "post_id": post_id, - "isDeleted": False - } - - filtered_content = self.supporter.wordfilter(content) - if filtered_content != content: - post_data["p"] = filtered_content - post_data["unfiltered_p"] = content - - if post_origin == "home": - result = self.filesystem.create_item("posts", post_id, post_data) - - if result: - payload = post_data - payload["mode"] = 1 - - self.cl.sendPacket({"cmd": "direct", "val": payload}) - return True - else: - return False - elif post_origin == "inbox": - post_data["type"] = 2 - - result = self.filesystem.create_item("posts", post_id, post_data) - - if result: - payload = { - "mode": "inbox_message", - "payload": {} - } - if user == "Server": - self.filesystem.db["usersv0"].update_many({"unread_inbox": False}, {"$set": {"unread_inbox": True}}) - self.cl.sendPacket({"cmd": "direct", "val": payload}) - elif user in self.cl.getUsernames(): - self.filesystem.db["usersv0"].update_many({"_id": user, "unread_inbox": False}, {"$set": {"unread_inbox": True}}) - self.cl.sendPacket({"cmd": "direct", "val": payload, "id": user}) - return True - else: - return False - elif post_origin == "livechat": - payload = post_data - payload["state"] = 2 - - self.cl.sendPacket({"cmd": "direct", "val": payload}) - return True - else: - result, chat_data = self.filesystem.load_item("chats", post_origin) - if result: - result = self.filesystem.create_item("posts", post_id, post_data) - - if result: - # Remove code below once client is updated - payload = post_data - payload["state"] = 2 - - for member in chat_data["members"]: - if member in self.cl.getUsernames(): - self.cl.sendPacket({"cmd": "direct", "val": payload, "id": member}) - return True - else: - return False - else: - return False - - def completeReport(self, _id, status): - if status == None: - self.filesystem.delete_item("reports", _id) - else: - FileRead, FileData = self.filesystem.load_item("reports", _id) - if FileRead: - if status == True: - for user in FileData["reports"]: - self.createPost("inbox", user, "We took action on one of your recent reports. Thank you for your help with keeping Meower a safe and welcoming place!") - elif status == False: - for user in FileData["reports"]: - self.createPost("inbox", user, "Sadly, we could not take action on one of your recent reports. The content you reported was not severe enough to warrant action being taken. We still want to thank you for your help with keeping Meower a safe and welcoming place!") - self.filesystem.delete_item("reports", _id) - - def returnCode(self, client, code, listener_detected, listener_id): - self.sendPacket({"cmd": "statuscode", "val": self.cl.codes[str(code)], "id": client}, listener_detected = listener_detected, listener_id = listener_id) - - # Networking/client utilities - - def ping(self, client, val, listener_detected, listener_id): - # Returns your ping for my pong - self.returnCode(client = client, code = "Pong", listener_detected = listener_detected, listener_id = listener_id) - - def version_chk(self, client, val, listener_detected, listener_id): - if type(val) == str: - # Load the supported versions list - result, payload = self.filesystem.load_item("config", "supported_versions") - if result: - if val in payload["index"]: - # If the client version string exists in the list, it is supported - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Either unsupported or out of date - self.returnCode(client = client, code = "ObsoleteClient", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - - def get_ulist(self, client, val, listener_detected, listener_id): - self.sendPacket({"cmd": "ulist", "val": self.cl._get_ulist(), "id": client}) - - # Accounts and security - - def authpswd(self, client, val, listener_detected, listener_id): - # Check if the client is already authenticated - if not self.supporter.isAuthenticated(client): - if type(val) == dict: - if ("username" in val) and ("pswd" in val): - - # Extract username and password for simplicity - username = val["username"] - password = val["pswd"] - ip = str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"]) - - if ((type(username) == str) and (type(password) == str)): - if not self.supporter.checkForBadCharsUsername(username): - if True: # not self.supporter.checkForBadCharsPost(password) - if not (self.supporter.check_for_spam("login", ip, burst=5, seconds=60)) or (self.supporter.check_for_spam("login", username, burst=5, seconds=60)): - FileCheck, FileRead, ValidAuth, Banned = self.accounts.authenticate(username, password) - if FileCheck and FileRead: - if ValidAuth: - try: - self.supporter.kickUser(username, status="IDConflict") # Kick bad clients missusing the username - except: - self.cl._closed_connection_server(self.cl._get_obj_of_username(val), self.cl) - - - self.filesystem.create_item("netlog", str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"]), {"users": [], "last_user": username}) - status, netlog = self.filesystem.load_item("netlog", str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"])) - if status: - if not username in netlog["users"]: - netlog["users"].append(username) - netlog["last_user"] = username - self.filesystem.write_item("netlog", str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"]), netlog) - FileCheck, FileRead, accountData = self.accounts.get_account(username, False, False) - token = secrets.token_urlsafe(64) - accountData["tokens"].append(token) - self.accounts.update_setting(username, {"last_ip": str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"]), "tokens": accountData["tokens"]}, forceUpdate=True) - self.supporter.autoID(client, username) # Give the client an AutoID - self.supporter.setAuthenticatedState(client, True) # Make the server know that the client is authed - # Return info to sender - payload = { - "mode": "auth", - "payload": { - "username": username, - "token": token - } - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}, listener_detected = listener_detected, listener_id = listener_id) - - # Tell the client it is authenticated - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - - # Log peak users - self.supporter.log_peak_users() - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - if Banned: - # Account banned - self.returnCode(client = client, code = "Banned", listener_detected = listener_detected, listener_id = listener_id) - else: - # Password invalid - self.returnCode(client = client, code = "PasswordInvalid", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account does not exist - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Ratelimited - self.returnCode(client = client, code = "RateLimit", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad characters being used - self.returnCode(client = client, code = "IllegalChars", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad characters being used - self.returnCode(client = client, code = "IllegalChars", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad syntax - self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Already authenticated - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - - def gen_account(self, client, val, listener_detected, listener_id): - # Check if the client is already authenticated - if not self.supporter.isAuthenticated(client): - if type(val) == dict: - if ("username" in val) and ("pswd" in val): - - # Extract username and password for simplicity - username = val["username"] - password = val["pswd"] - ip = str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"]) - - if ((type(username) == str) and (type(password) == str)): - if not (len(username) > 20) or (password > 255): - if not self.supporter.checkForBadCharsUsername(username): - # Check if the IP is a VPN/proxy - if ip in self.supporter.known_vpns: - return self.returnCode(client = client, code = "Blocked", listener_detected = listener_detected, listener_id = listener_id) - elif ip not in self.supporter.good_ips: - iphub_key = os.getenv("IPHUB_KEY") - if iphub_key != "": - ip_info = requests.get("http://v2.api.iphub.info/ip/{0}".format(ip), headers={"X-Key": iphub_key}) - if ip_info.status_code == 200: - if ip_info.json()["block"] == 1: - self.log("{0} was detected as a VPN/proxy".format(ip)) - self.supporter.known_vpns.add(ip) - return self.returnCode(client = client, code = "Blocked", listener_detected = listener_detected, listener_id = listener_id) - else: - self.supporter.good_ips.add(ip) - else: - self.log("{0} was detected using an invalid IP") - return self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.log("No IPHub API key detected, skipping VPN/proxy check for {0}".format(ip)) - - if not self.supporter.check_for_spam("signup", ip, burst=2, seconds=120): - FileCheck, FileWrite = self.accounts.create_account(username, password) - - if FileCheck and FileWrite: - self.filesystem.create_item("netlog", str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"]), {"users": [], "last_user": username}) - status, netlog = self.filesystem.load_item("netlog", str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"])) - if status: - if not username in netlog["users"]: - netlog["users"].append(username) - netlog["last_user"] = username - self.filesystem.write_item("netlog", str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"]), netlog) - FileCheck, FileRead, accountData = self.accounts.get_account(username, False, False) - token = secrets.token_urlsafe(64) - accountData["tokens"].append(token) - self.accounts.update_setting(username, {"last_ip": str(self.cl.statedata["ulist"]["objs"][client["id"]]["ip"]), "tokens": accountData["tokens"]}, forceUpdate=True) - self.supporter.autoID(client, username) # If the client is JS-based then give them an AutoID - self.supporter.setAuthenticatedState(client, True) # Make the server know that the client is authed - - # Return info to sender - payload = { - "mode": "auth", - "payload": { - "username": username, - "token": token - } - } - - self.sendPacket({"cmd": "direct", "val": payload, "id": client}, listener_detected = listener_detected, listener_id = listener_id) - - # Tell the client it is authenticated - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - - # Log peak users - self.supporter.log_peak_users() - - # Send welcome message - self.createPost(post_origin="inbox", user=username, content="Welcome to Meower! We welcome you with open arms! You can get started by making friends in the global chat or home, or by searching for people and adding them to a group chat. We hope you have fun!") - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileWrite): - # Account already exists - self.returnCode(client = client, code = "IDExists", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Ratelimited - self.returnCode(client = client, code = "RateLimit", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad characters being used - self.returnCode(client = client, code = "IllegalChars", listener_detected = listener_detected, listener_id = listener_id) - else: - # Message too large - self.returnCode(client = client, code = "TooLarge", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad syntax - self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Already authenticated - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - - def get_profile(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - FileCheck, FileRead, Payload = self.accounts.get_account(val, (val != client), True) - - if FileCheck and FileRead: - payload = { - "mode": "profile", - "payload": Payload, - "user_id": val - } - - self.log("{0} fetching profile {1}".format(client, val)) - self.sendPacket({"cmd": "direct", "val": payload, "id": client}, listener_detected = listener_detected, listener_id = listener_id) - - # Return to the client it's data - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def update_config(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == dict: - FileCheck, FileRead, Payload = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - self.log("{0} updating config".format(client)) - FileCheck, FileRead, FileWrite = self.accounts.update_setting(client, val) - if FileCheck and FileRead and FileWrite: - # OK - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - # General - - def get_home(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if (type(val) == dict) and ("page" in val) and self.checkForInt(val["page"]): - page = int(val["page"]) - else: - page = 1 - home_index = self.getIndex("posts", {"post_origin": "home", "isDeleted": False}, truncate=True, page=page) - for i in range(len(home_index["index"])): - home_index["index"][i] = home_index["index"][i]["_id"] - payload = { - "mode": "home", - "payload": home_index - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def post_home(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - if not len(val) > 4000: - if not self.supporter.check_for_spam("posts", client, burst=6, seconds=5): - # Create post - result = self.createPost(post_origin="home", user=client, content=val) - if result: - self.log("{0} posting home message".format(client)) - # Tell client message was sent - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - self.supporter.ratelimit(client) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Rate limiter - self.returnCode(client = client, code = "RateLimit", listener_detected = listener_detected, listener_id = listener_id) - else: - # Message too large - self.returnCode(client = client, code = "TooLarge", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def get_post(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - result, payload = self.filesystem.load_item("posts", val) - if result: - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - hasPermission = False - if accountData["lvl"] >= 1: - hasPermission = True - else: - if payload["post_origin"] == "home": - hasPermission = True - elif (payload["post_origin"] == "inbox") and ((payload["u"] == client) or (payload["u"] == "Server")): - hasPermission = True - else: - result, chatdata = self.filesystem.load_item("chats", payload["post_origin"]) - if result: - if client in chatdata["members"]: - hasPermission = True - if hasPermission: - if payload["isDeleted"] and accountData["lvl"] < 1: - payload = { - "mode": "post", - "payload": { - "isDeleted": True - } - } - else: - payload = { - "mode": "post", - "payload": payload - } - - self.log("{0} getting post {1}".format(client, val)) - - # Relay post to client - self.sendPacket({"cmd": "direct", "val": payload, "id": client}) - - # Tell client message was sent - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - # Logging and data management - - def get_peak_users(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - payload = { - "mode": "peak", - "payload": self.supporter.peak_users_logger - } - - # Relay data to client - self.sendPacket({"cmd": "direct", "val": payload, "id": client}) - - # Tell client data was sent - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def search_user_posts(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == dict: - if ("query" in val) and (type(val["query"]) == str): - if ("page" in val) and self.checkForInt(val["page"]): - page = int(val["page"]) - else: - page = 1 - - post_index = self.getIndex(location="posts", query={"post_origin": "home", "u": val["query"], "isDeleted": False}, truncate=True, page=page) - for i in range(len(post_index["index"])): - post_index["index"][i] = post_index["index"][i]["_id"] - post_index["index"].reverse() - payload = { - "mode": "user_posts", - "index": post_index - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad syntax - self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - # Moderator features - - def report(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == dict: - if ("type" in val) and ("id" in val): - if (type(val["type"]) == int) and (type(val["id"]) == str): - if val["type"] == 0: - if not self.filesystem.does_item_exist("posts", val["id"]): - return self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - elif val["type"] == 1: - if not self.filesystem.does_item_exist("usersv0", val["id"]): - return self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - return self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - - if self.filesystem.does_item_exist("reports", val["id"]): - FileRead, reportData = self.filesystem.load_item("reports", val["id"]) - if FileRead: - if client not in reportData["reports"]: - reportData["reports"].append(client) - FileWrite = self.filesystem.write_item("reports", val["id"], reportData) - if FileWrite: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - FileWrite = self.filesystem.create_item("reports", val["id"], {"type": val["type"], "reports": [client]}) - if FileWrite: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad syntax - self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def close_report(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 1: - if type(val) == str: - self.completeReport(val, False) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def clear_home(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 1: - if (type(val) == dict) and ("page" in val) and self.checkForInt(val["page"]): - page = int(val["page"]) - else: - page = 1 - home_index = self.getIndex("posts", {"post_origin": "home", "isDeleted": False}, truncate=True, page=page) - for post in home_index["index"]: - post["isDeleted"] = True - self.filesystem.write_item("posts", post["_id"], post) - self.completeReport(val, None) - # Return to the client it's data - self.sendPacket({"cmd": "direct", "val": "", "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def clear_user_posts(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 1: - # Delete all posts - post_index = self.getIndex("posts", {"post_origin": "home", "u": str(val), "isDeleted": False}, truncate=False) - for post in post_index["index"]: - post["isDeleted"] = True - self.filesystem.write_item("posts", post["_id"], post) - self.completeReport(post["_id"], True) - self.sendPacket({"cmd": "direct", "val": {"mode": "delete", "id": post["_id"]}}) - # Give report feedback - self.completeReport(val, True) - # Send alert to user - self.createPost(post_origin="inbox", user=str(val), content="All your home posts have been deleted by a moderator. If you think this is a mistake, please contact the Meower moderation team.") - # Return to the client it's data - self.sendPacket({"cmd": "direct", "val": "", "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def alert(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 1: - if type(val) == dict: - if ("username" in val) and ("p" in val): - if (type(val["username"]) == str) and (type(val["p"]) == str): - if self.accounts.account_exists(val["username"]): - - self.completeReport(val["username"], True) - - # Give report feedback - self.completeReport(val, True) - - # Send alert - - self.createPost(post_origin="inbox", user=val["username"], content="Message from a moderator: {0}".format(val["p"])) - - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad syntax - self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def announce(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 3: - if type(val) == str: - self.createPost(post_origin="inbox", user="Server", content=val) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def block(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 2: - if type(val) == str: - result, payload = self.filesystem.load_item("config", "IPBanlist") - if result: - if val not in payload["wildcard"]: - self.log("Wildcard unblocking IP address {0}".format(val)) - payload["wildcard"].append(val) - self.cl.blockIP(val) - - # Kick all clients - FileRead, netlog = self.filesystem.load_item("netlog", val) - if FileRead: - for user in netlog["users"]: - if user in self.cl.getUsernames() and (self.cl.statedata["ulist"]["objs"][self.cl.statedata["ulist"]["usernames"][user]]["ip"] == val): - try: - self.supporter.kickUser(user, "Blocked") - except: - self.cl._closed_connection_server(self.cl._get_obj_of_username(user), self.cl) - - result = self.filesystem.write_item("config", "IPBanlist", payload) - if result: - self.sendPacket({"cmd": "direct", "val": "", "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def unblock(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 2: - if type(val) == str: - result, payload = self.filesystem.load_item("config", "IPBanlist") - if result: - if val in payload["wildcard"]: - self.log("Wildcard unblocking IP address {0}".format(val)) - payload["wildcard"].remove(val) - self.cl.unblockIP(val) - - result = self.filesystem.write_item("config", "IPBanlist", payload) - if result: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def kick(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 1: - if type(val) == str: - if val in self.cl.getUsernames(): - # Revoke sessions - FileCheck, FileRead, FileWrite = self.accounts.update_setting(val, {"tokens": []}, forceUpdate=True) - if FileCheck and FileRead and FileWrite: - # Kick the user - try: - self.supporter.kickUser(val) - except: - self.cl._closed_connection_server(self.cl._get_obj_of_username(val), self.cl) - - # Tell client it kicked the user - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # User not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def get_user_ip(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 2: - if type(val) == str: - FileCheck, FileRead, userdata = self.accounts.get_account(val) - if FileCheck and FileRead: - payload = { - "mode": "user_ip", - "payload": { - "username": str(val), - "ip": str(userdata["last_ip"]) - } - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def get_ip_data(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 2: - if type(val) == str: - if self.filesystem.does_item_exist("netlog", str(val)): - result, netdata = self.filesystem.load_item("netlog", str(val)) - if result: - result, banlist = self.filesystem.load_item("config", "IPBanlist") - if result: - netdata["banned"] = (str(val) in banlist["wildcard"]) - netdata["ip"] = str(val) - payload = { - "mode": "ip_data", - "payload": netdata - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # IP not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def get_user_data(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 1: - if type(val) == str: - if self.accounts.account_exists(val): - FileCheck, FileRead, userdata = self.accounts.get_account(val, False, True) - if FileCheck and FileRead: - userdata["username"] = str(val) - payload = { - "mode": "user_data", - "payload": userdata - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account does not exist - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # User not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def ban(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 1: - if type(val) == str: - if self.accounts.account_exists(val): - FileCheck, FileRead, FileWrite = self.accounts.update_setting(val, {"banned": True}, forceUpdate=True) - if FileCheck and FileRead and FileWrite: - self.createPost(post_origin="inbox", user=val, content="Your account has been banned due to recent activity. If you think this is a mistake, please report this message and we will look further into it.") - self.log("Banning {0}".format(val)) - # Kick client - self.supporter.kickUser(val, status="Banned") - - # Give report feedback - self.completeReport(val, True) - - # Tell client it banned the user - self.sendPacket({"cmd": "direct", "val": "", "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # User not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def pardon(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 1: - if type(val) == str: - if self.accounts.account_exists(val): - FileCheck, FileRead, FileWrite = self.accounts.update_setting(val, {"banned": False}, forceUpdate=True) - if FileCheck and FileRead and FileWrite: - self.createPost(post_origin="inbox", user=val, content="Your account has been unbanned. Welcome back! Please make sure to follow the Meower community guidelines in the future, otherwise you may receive a more severe punishment.") - self.log("Pardoning {0}".format(val)) - # Tell client it pardoned the user - self.sendPacket({"cmd": "direct", "val": "", "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # User not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def terminate(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 3: - if self.filesystem.does_item_exist("usersv0", val): - # Delete all posts - post_index = self.getIndex("posts", {"u": str(val), "isDeleted": False}, truncate=False) - for post in post_index["index"]: - post["isDeleted"] = True - self.filesystem.write_item("posts", post["_id"], post) - if post["post_origin"] != "inbox": - self.completeReport(post["_id"], True) - self.sendPacket({"cmd": "direct", "val": {"mode": "delete", "id": post["_id"]}}) - FileCheck, FileRead, FileWrite = self.accounts.update_setting(val, {"banned": True}, forceUpdate=True) - if FileCheck and FileRead and FileWrite: - self.log("Terminating {0}".format(val)) - # Kick the user - self.cl.kickClient(val, status="Banned") - - # Give report feedback - self.completeReport(val, True) - - # Tell client it terminated the user - self.sendPacket({"cmd": "direct", "val": "", "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def repair_mode(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 4: - self.log("Enabling repair mode") - # Save repair mode status to database and memory - self.filesystem.write_item("config", "status", {"repair_mode": True, "is_deprecated": False}) - self.supporter.status = {"repair_mode": True, "is_deprecated": False} - # Tell client it enabled repair mode - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - # Kick all online users - self.log("Kicking all clients") - for username in self.cl.getUsernames(): - try: - self.cl.kickClient(username) - except: - pass - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - # Chat-related - - def delete_post(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if self.filesystem.does_item_exist("posts", val): - result, payload = self.filesystem.load_item("posts", val) - if result: - if (payload["post_origin"] != "inbox") and ((payload["u"] == client) or ((payload["u"] == "Discord") and payload["p"].startswith("{0}:".format(client)))): - payload["isDeleted"] = True - result = self.filesystem.write_item("posts", val, payload) - if result: - self.log("{0} deleting post {1}".format(client, val)) - - # Relay post deletion to clients - self.sendPacket({"cmd": "direct", "val": {"mode": "delete", "id": val}}) - - # Return to the client the post was deleted - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] >= 1: - if type(val) == str: - payload["isDeleted"] = True - payload["mod_deleted"] = True - result = self.filesystem.write_item("posts", val, payload) - if result: - self.log("{0} deleting post {1}".format(client, val)) - - # Relay post deletion to clients - self.sendPacket({"cmd": "direct", "val": {"mode": "delete", "id": val}}) - - # Create moderator alert - if payload["post_origin"] != "inbox": - self.createPost(post_origin="inbox", user=payload["u"], content="One of your posts were removed by a moderator because it violated the Meower terms of service! If you think this is a mistake, please report this message and we will look further into it. Post: '{0}'".format(payload["p"])) - - # Give report feedback - self.completeReport(payload["_id"], True) - - # Return to the client the post was deleted - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Post not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - - - - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def create_chat(self, client, val, listener_detected, listener_id): - # Check if the client is already authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - if not len(val) > 20: - val = self.supporter.wordfilter(val) - result = self.filesystem.create_item("chats", str(uuid.uuid4()), {"nickname": val, "owner": client, "members": [client]}) - if result: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad syntax - self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def leave_chat(self, client, val, listener_detected, listener_id): - # Check if the client is already authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - if not len(val) > 50: - if self.filesystem.does_item_exist("chats", val): - result, payload = self.filesystem.load_item("chats", val) - if result: - if client in payload["members"]: - if payload["owner"] == client: - result = self.filesystem.delete_item("chats", val) - for member in payload["members"]: - if member in self.cl.getUsernames(): - self.sendPacket({"cmd": "direct", "val": {"mode": "delete", "id": payload["_id"]}, "id": member}) - if result: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - payload["members"].remove(client) - result = self.filesystem.write_item("chats", val, payload) - if result: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Too large - self.returnCode(client = client, code = "TooLarge", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def get_chat_list(self, client, val, listener_detected, listener_id): - # Check if the client is already authenticated - if self.supporter.isAuthenticated(client): - if (type(val) == dict) and ("page" in val) and self.checkForInt(val["page"]): - page = int(val["page"]) - else: - page = 1 - chat_index = self.getIndex(location="chats", query={"members": {"$all": [client]}}, truncate=True, page=page, sort="nickname") - chat_index["all_chats"] = [] - for i in range(len(chat_index["index"])): - chat_index["all_chats"].append(chat_index["index"][i]) - chat_index["index"][i] = chat_index["index"][i]["_id"] - chat_index["index"].reverse() - chat_index["all_chats"].reverse() - payload = { - "mode": "chats", - "payload": chat_index - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def get_chat_data(self, client, val, listener_detected, listener_id): - # Check if the client is already authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - if not len(val) > 50: - if self.filesystem.does_item_exist("chats", val): - result, chatdata = self.filesystem.load_item("chats", val) - if result: - if client in chatdata["members"]: - payload = { - "mode": "chat_data", - "payload": { - "chatid": chatdata["_id"], - "nickname": chatdata["nickname"], - "owner": chatdata["owner"], - "members": chatdata["members"] - } - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "TooLarge", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def get_chat_posts(self, client, val, listener_detected, listener_id): - # Check if the client is already authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - if not len(val) > 50: - if self.filesystem.does_item_exist("chats", val): - result, chatdata = self.filesystem.load_item("chats", val) - if result: - if client in chatdata["members"]: - posts_index = self.getIndex(location="posts", query={"post_origin": val, "isDeleted": False}, truncate=True) - for i in range(len(posts_index["index"])): - posts_index["index"][i] = posts_index["index"][i]["_id"] - print(posts_index) - payload = { - "mode": "chat_posts", - "payload": posts_index - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "TooLarge", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def set_chat_state(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if not self.supporter.isAuthenticated(client): # Not authenticated - return self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - elif not ((type(val) == dict) and (("state" in val) and self.checkForInt(val["state"]) and (("chatid" in val) and (type(val["chatid"]) == str)))): # Bad datatype - return self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - - # Extract state and chat ID for simplicity - state = int(val["state"]) - chatid = val["chatid"] - - # Some messy permission checking - if chatid == "livechat": - pass - else: - FileRead, chatdata = self.filesystem.load_item("chats", chatid) - if not FileRead: - if not self.filesystem.does_item_exist("chats", chatid): - # Chat doesn't exist - return self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error - return self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - if not (client in chatdata["members"]): - # User not in chat - return self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - - # Create post format - post_w_metadata = {} - post_w_metadata["state"] = state - post_w_metadata["u"] = str(client) - post_w_metadata["chatid"] = str(chatid) - - self.log("{0} modifying {1} state to {2}".format(client, chatid, state)) - - if chatid == "livechat": - self.sendPacket({"cmd": "direct", "val": post_w_metadata}) - else: - for member in chatdata["members"]: - self.sendPacket({"cmd": "direct", "val": post_w_metadata, "id": member}) - - # Tell client message was sent - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - - # Rate limit user - self.supporter.ratelimit(client) - - def post_chat(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if (type(val) == dict) and (("p" in val) and (type(val["p"]) == str)) and (("chatid" in val) and (type(val["chatid"]) == str)): - post = val["p"] - chatid = val["chatid"] - if (not len(post) > 2000) and (not len(chatid) > 50): - if not self.supporter.check_for_spam("posts", client, burst=6, seconds=5): - if chatid == "livechat": - result = self.createPost(post_origin=chatid, user=client, content=post) - if result: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - self.supporter.ratelimit(client) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - result, chat_data = self.filesystem.load_item("chats", chatid) - if result: - if client in chat_data["members"]: - result = self.createPost(post_origin=chatid, user=client, content=post) - if result: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - self.supporter.ratelimit(client) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Rate limiter - self.returnCode(client = client, code = "RateLimit", listener_detected = listener_detected, listener_id = listener_id) - else: - # Message too large - self.returnCode(client = client, code = "TooLarge", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def add_to_chat(self, client, val, listener_detected, listener_id): - # Check if the client is already authenticated - if self.supporter.isAuthenticated(client): - if type(val) == dict: - if (("username" in val) and (type(val["username"]) == str)) and (("chatid" in val) and (type(val["chatid"]) == str)): - username = val["username"] - chatid = val["chatid"] - - if not self.supporter.check_for_spam("update_chat", client, burst=5, seconds=3): - # Read chat UUID's nickname - FileRead, chatdata = self.filesystem.load_item("chats", chatid) - if FileRead: - if client in chatdata["members"]: - # Check if the group chat is full - if len(chatdata["members"]) < 256: - # Check if the user exists - if self.filesystem.does_item_exist("usersv0", username): - # Add user to group chat - if (username not in chatdata["members"]) and (username != "Server"): - chatdata["members"].append(username) - FileWrite = self.filesystem.write_item("chats", chatid, chatdata) - - if FileWrite: - # Inbox message to say the user was added to the group chat - self.createPost("inbox", username, "You have been added to the group chat '{0}' by @{1}!".format(chatdata["nickname"], client)) - - # Tell client user was added - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "IDExists", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "ChatFull", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Rate limiter - self.returnCode(client = client, code = "RateLimit", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad syntax - self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def remove_from_chat(self, client, val, listener_detected, listener_id): - # Check if the client is already authenticated - if self.supporter.isAuthenticated(client): - if type(val) == dict: - if (("username" in val) and (type(val["username"]) == str)) and (("chatid" in val) and (type(val["chatid"]) == str)): - username = val["username"] - chatid = val["chatid"] - - if not self.supporter.check_for_spam("update_chat", client, burst=5, seconds=3): - # Read chat UUID's nickname - result, chatdata = self.filesystem.load_item("chats", chatid) - if result: - if client == chatdata["owner"]: - if (client != username) and (username != "Server"): - # Remove user from group chat - chatdata["members"].remove(username) - result = self.filesystem.write_item("chats", chatid, chatdata) - - if result: - # Inbox message to say the user was removed from the group chat - self.createPost("inbox", username, "You have been removed from the group chat '{0}' by @{1}!".format(chatdata["nickname"], client)) - - # Tell client user was added - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Rate limiter - self.returnCode(client = client, code = "RateLimit", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad syntax - self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def get_inbox(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == dict: - if ("page" in val) and self.checkForInt(val["page"]): - page = int(val["page"]) - else: - page = 1 - - inbox_index = self.getIndex(location="posts", query={"post_origin": "inbox", "u": {"$in": [client, "Server"]}, "isDeleted": False}, page=page) - for i in range(len(inbox_index["index"])): - inbox_index["index"][i] = inbox_index["index"][i]["_id"] - inbox_index["index"].reverse() - payload = { - "mode": "inbox", - "payload": inbox_index - } - self.sendPacket({"cmd": "direct", "val": payload, "id": client}, listener_detected = listener_detected, listener_id = listener_id) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def change_pswd(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == dict: - if ("old" in val) and ("new" in val): - if (type(val["old"]) == str) and (type(val["new"]) == str): - old_password = val["old"] - new_password = val["new"] - if not self.supporter.check_for_spam("password-change", client, burst=2, seconds=120): - if (len(old_password) <= 255) and (len(new_password) <= 255): - # Check old password - FileCheck, FileRead, ValidAuth, Banned = self.accounts.authenticate(client, old_password) - if FileCheck and FileRead: - if ValidAuth: - # Change password - FileCheck, FileRead, FileWrite = self.accounts.change_password(client, new_password) - if FileCheck and FileRead and FileWrite: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "PasswordInvalid", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "TooLarge", listener_detected = listener_detected, listener_id = listener_id) - else: - # Ratelimited - self.returnCode(client = client, code = "RateLimit", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad syntax - self.returnCode(client = client, code = "Syntax", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def del_tokens(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - FileCheck, FileRead, FileWrite = self.accounts.update_setting(client, {"tokens": []}, forceUpdate=True) - if FileCheck and FileRead and FileWrite: - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) - - def del_account(self, client, val, listener_detected, listener_id): - # Check if the client is authenticated - if self.supporter.isAuthenticated(client): - if type(val) == str: - if len(val) <= 255: - if not self.supporter.check_for_spam("login", client, burst=5, seconds=60): - # Check old password - FileCheck, FileRead, ValidAuth, Banned = self.accounts.authenticate(client, val) - if FileCheck and FileRead: - if ValidAuth: - # Delete account - FileCheck, FileRead, accountData = self.accounts.get_account(client, True, True) - if FileCheck and FileRead: - if accountData["lvl"] == 0: - all_posts = self.getIndex(location="posts", query={"u": client}, truncate=False)["index"] - for post in all_posts: - self.filesystem.delete_item("posts", post["_id"]) - self.completeReport(post["_id"], None) - if post["post_origin"] != "inbox": - self.sendPacket({"cmd": "direct", "val": {"mode": "delete", "id": post["_id"]}}) - chat_index = self.getIndex(location="chats", query={"members": {"$all": [client]}}, truncate=False)["index"] - for chat in chat_index: - if chat["owner"] == client: - self.filesystem.delete_item("chats", chat["_id"]) - for member in chat["members"]: - if member in self.cl.getUsernames(): - self.sendPacket({"cmd": "direct", "val": {"mode": "delete", "id": chat["_id"]}, "id": member}) - else: - chat["members"].remove(client) - self.filesystem.write_item("chats", chat["_id"], chat) - netlog_index = self.getIndex(location="netlog", query={"users": {"$all": [client]}}, truncate=False)["index"] - for ip in netlog_index: - ip["users"].remove(client) - if len(ip["users"]) == 0: - self.filesystem.delete_item("netlog", ip["_id"]) - else: - if ip["last_user"] == client: - ip["last_user"] = ip["users"][(len(ip["users"])-1)] - self.filesystem.write_item("netlog", ip["_id"], ip) - self.filesystem.delete_item("usersv0", client) - self.completeReport(client, None) - self.returnCode(client = client, code = "OK", listener_detected = listener_detected, listener_id = listener_id) - time.sleep(1) - self.cl.kickClient(client) - else: - # User cannot delete account as an admin - self.returnCode(client = client, code = "MissingPermissions", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Password invalid - self.returnCode(client = client, code = "PasswordInvalid", listener_detected = listener_detected, listener_id = listener_id) - else: - if ((not FileCheck) and FileRead): - # Account not found - self.returnCode(client = client, code = "IDNotFound", listener_detected = listener_detected, listener_id = listener_id) - else: - # Some other error, raise an internal error. - self.returnCode(client = client, code = "InternalServerError", listener_detected = listener_detected, listener_id = listener_id) - else: - # Ratelimited - self.returnCode(client = client, code = "RateLimit", listener_detected = listener_detected, listener_id = listener_id) - else: - self.returnCode(client = client, code = "TooLarge", listener_detected = listener_detected, listener_id = listener_id) - else: - # Bad datatype - self.returnCode(client = client, code = "Datatype", listener_detected = listener_detected, listener_id = listener_id) - else: - # Not authenticated - self.returnCode(client = client, code = "Refused", listener_detected = listener_detected, listener_id = listener_id) diff --git a/requirements.txt b/requirements.txt index 2fc5d77..c3b75df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ -CloudLink>=0.1.7.4 -bcrypt +python-dotenv +pymongo +redis +ujson requests -flask-cors -uuid +bcrypt flask -pymongo -python-dotenv +flask-cors +websockets \ No newline at end of file diff --git a/rest_api.py b/rest_api.py deleted file mode 100644 index ece2a2f..0000000 --- a/rest_api.py +++ /dev/null @@ -1,398 +0,0 @@ -from flask import Flask, request -from flask_cors import CORS -from security import Security -from supporter import Supporter -from meower import Meower -from files import Files - -app = Flask(__name__, static_folder="static") -cors = CORS(app, resources=r'*') - -# Init libraries -supporter = Supporter() -filesystem = Files( - logger = supporter.log, - errorhandler = supporter.full_stack -) -accounts = Security( - files = filesystem, - supporter = supporter, - logger = supporter.log, - errorhandler = supporter.full_stack -) -meower = Meower( - supporter = supporter, - cl = None, - logger = supporter.log, - errorhandler = supporter.full_stack, - accounts = accounts, - files = filesystem -) - -def fetch_post_from_storage(post_id): - if filesystem.does_item_exist("posts", post_id): - result, payload = filesystem.load_item("posts", post_id) - - if result: - if (payload["post_origin"] != "home") or payload["isDeleted"]: - payload = { - "isDeleted": True - } - else: - payload["isDeleted"] = False - - return True, result, payload - else: - return True, False, {} - -@app.before_request -def pre_request_check_auth(): - request.user = None - request.lvl = 0 - if ("username" in request.headers) and ("token" in request.headers): - username = request.headers.get("username") - if len(username) > 20: - username = username[:20] - token = request.headers.get("token") - if len(token) > 86: - token = token[:86] - filecheck, fileread, filedata = accounts.get_account(username, False, False) - if filecheck and fileread: - if (token in filedata["tokens"]) and (filedata["banned"] == False): - request.user = filedata["_id"] - request.lvl = filedata["lvl"] - -@app.route('/', methods = ['GET']) # Index -def index(): - if request.method == "GET": - return "Hello world! The Meower API is working, but it's under construction. Please come back later.", 200 - -@app.route('/ip', methods = ['GET']) # Get the Cloudflare IP address -def ip_tracer(): - if request.method == "GET": - if "Cf-Connecting-Ip" in request.headers: - return str(request.headers["Cf-Connecting-Ip"]), 200 - else: - return str(request.remote_addr) - else: - return {"error": True, "type": "notAllowed"}, 405 - -@app.route('/favicon.ico', methods = ['GET']) # Favicon, my ass. We need no favicon for an API. -def favicon_my_ass(): - return '', 200 - -@app.route('/posts', methods=["GET"]) -def get_post(): - post_id = "" - args = request.args - if "id" in args: - post_id = args.get("id") - filecheck, fileget, filedata = fetch_post_from_storage(post_id) - if filecheck and fileget: - filedata["error"] = False - return filedata, 200 - else: - if filecheck and (not fileget): - return {"error": True, "type": "notFound"}, 404 - else: - return {"error": True, "type": "Internal"}, 500 - else: - return {"error": True, "type": "noQueryString"}, 200 - -@app.route('/posts/', methods=["GET"]) -def get_mychat_posts(chatid): - page = 1 - autoget = False - args = request.args - - if "page" in args: - page = args.get("page") - try: - page = int(page) - except: - return {"error": True, "type": "Datatype"}, 500 - - if "autoget" in args: - autoget = True - - if request.user is None: - return {"error": True, "type": "Unauthorized"}, 401 - - fileread, filedata = filesystem.load_item("chats", chatid) - if fileread: - if request.user not in filedata["members"]: - return {"error": True, "type": "Forbidden"}, 403 - else: - return {"error": True, "type": "notFound"}, 404 - - payload = meower.getIndex(location="posts", query={"post_origin": chatid, "isDeleted": False}, truncate=True, page=page) - if not autoget: - for i in range(len(payload["index"])): - payload["index"][i] = payload["index"][i]["_id"] - payload["error"] = False - return payload, 200 - else: - supporter.log("Loaded index, data {0}".format(payload)) - try: - tmp_payload = {"error": False, "autoget": [], "page#": payload["page#"], "pages": payload["pages"]} - tmp_payload["autoget"] = payload["index"] - - return tmp_payload, 200 - except: - return {"error": True, "type": "Internal"}, 500 - -@app.route('/home', methods=["GET"]) -def get_home(): - page = 1 - args = request.args - - if "page" in args: - page = args.get("page") - try: - page = int(page) - - if (page > 1) and (request.user is None): - return {"error": True, "type": "Unauthorized"}, 401 - else: - supporter.log("{0} requested to get page {1} of home".format(request.user, page)) - except: - return {"error": True, "type": "Datatype"}, 500 - - try: - posts = meower.getIndex(location="posts", query={"post_origin": "home", "isDeleted": False}, truncate=True, page=page) - payload = {"error": False, "autoget": [], "page#": posts["page#"], "pages": (1 if (request.user is None) else posts["pages"])} - payload["autoget"] = posts["index"] - - return payload, 200 - except: - return {"error": True, "type": "Internal"}, 500 - -@app.route('/reports', methods=["GET"]) -def get_reports(): - page = 1 - args = request.args - - if "page" in args: - page = args.get("page") - try: - page = int(page) - except: - return {"error": True, "type": "Datatype"}, 500 - - if (request.user == None) or (request.lvl < 1): - return {"error": True, "type": "Unauthorized"}, 401 - - payload = meower.getIndex(location="reports", query={}, truncate=True, page=page) - supporter.log("Loaded index, data {0}".format(payload)) - try: - tmp_payload = {"error": False, "autoget": [], "page#": payload["page#"], "pages": payload["pages"]} - for item in payload["index"]: - if item["type"] == 0: - fileread, filedata = filesystem.load_item("posts", item["_id"]) - if fileread: - filedata["type"] = 0 - tmp_payload["autoget"].append(filedata) - else: - continue - elif item["type"] == 1: - filecheck, fileget, filedata = accounts.get_account(item["_id"], True, True) - if filecheck and fileget: - filedata["type"] = 1 - tmp_payload["autoget"].append(filedata) - else: - continue - - return tmp_payload, 200 - except: - return {"error": True, "type": "Internal"}, 500 - -@app.route('/inbox', methods=["GET"]) -def get_inbox(): - page = 1 - autoget = False - args = request.args - - if "page" in args: - page = args.get("page") - try: - page = int(page) - except: - return {"error": True, "type": "Datatype"}, 500 - - if "autoget" in args: - autoget = True - - if request.user is None: - return {"error": True, "type": "Unauthorized"}, 401 - - payload = meower.getIndex(location="posts", query={"post_origin": "inbox", "u": {"$in": [request.user, "Server"]}, "isDeleted": False}, truncate=True, page=page) - if not autoget: - for i in range(len(payload["index"])): - payload["index"][i] = payload["index"][i]["_id"] - payload["error"] = False - return payload, 200 - else: - supporter.log("Loaded index, data {0}".format(payload)) - try: - tmp_payload = {"error": False, "autoget": [], "page#": payload["page#"], "pages": payload["pages"]} - tmp_payload["autoget"] = payload["index"] - - return tmp_payload, 200 - except: - return {"error": True, "type": "Internal"}, 500 - -@app.route('/search/home', methods=["GET"]) -def search_home(): - page = 1 - autoget = False - args = request.args - - if "page" in args: - page = args.get("page") - try: - page = int(page) - except: - return {"error": True, "type": "Datatype"}, 500 - - if "autoget" in args: - autoget = True - - if "q" in args: - query = args.get("q") - else: - return {"error": True, "type": "Syntax"}, 400 - - if len(query) > 360: - query = query[:360] - - payload = meower.getIndex(location="posts", query={"post_origin": "home", "p": {"$regex": query}, "isDeleted": False}, truncate=True, page=page) - if not autoget: - for i in range(len(payload["index"])): - payload["index"][i] = payload["index"][i]["_id"] - payload["error"] = False - return payload, 200 - else: - supporter.log("Loaded index, data {0}".format(payload)) - try: - tmp_payload = {"error": False, "autoget": [], "page#": payload["page#"], "pages": payload["pages"]} - tmp_payload["autoget"] = payload["index"] - - return tmp_payload, 200 - except: - return {"error": True, "type": "Internal"}, 500 - -@app.route('/search/users', methods=["GET"]) -def search_users(): - page = 1 - autoget = False - args = request.args - - if "page" in args: - page = args.get("page") - try: - page = int(page) - except: - return {"error": True, "type": "Datatype"}, 400 - - if "autoget" in args: - autoget = True - - if "q" in args: - query = args.get("q") - else: - return {"error": True, "type": "Syntax"}, 400 - - if len(query) > 20: - query = query[:20] - - payload = meower.getIndex(location="usersv0", query={"lower_username": {"$regex": query.lower()}}, truncate=True, page=page, sort="lower_username") - if not autoget: - for i in range(len(payload["index"])): - payload["index"][i] = payload["index"][i]["_id"] - payload["error"] = False - return payload, 200 - else: - supporter.log("Loaded index, data {0}".format(payload)) - try: - tmp_payload = {"error": False, "autoget": [], "page#": payload["page#"], "pages": payload["pages"]} - for user in payload["index"]: - filecheck, fileget, filedata = accounts.get_account(user["_id"], True, True) - if filecheck and fileget: - tmp_payload["autoget"].append(filedata) - else: - continue - - return tmp_payload, 200 - except: - return {"error": True, "type": "Internal"}, 500 - -@app.route('/users/', methods=["GET"]) -def get_user(username): - filecheck, fileget, filedata = accounts.get_account(username, True, True) - if filecheck and fileget: - filedata["error"] = False - return filedata, 200 - else: - return {"error": True, "type": "notFound"}, 404 - -@app.route('/users//posts', methods=["GET"]) -def get_user_posts(username): - page = 1 - autoget = False - args = request.args - - if "page" in args: - page = args.get("page") - try: - page = int(page) - except: - return {"error": True, "type": "Datatype"}, 500 - - if "autoget" in args: - autoget = True - - payload = meower.getIndex(location="posts", query={"post_origin": "home", "u": username, "isDeleted": False}, truncate=True, page=page) - if not autoget: - for i in range(len(payload["index"])): - payload["index"][i] = payload["index"][i]["_id"] - payload["error"] = False - return payload, 200 - else: - supporter.log("Loaded index, data {0}".format(payload)) - try: - tmp_payload = {"error": False, "autoget": [], "page#": payload["page#"], "pages": payload["pages"]} - tmp_payload["autoget"] = payload["index"] - - return tmp_payload, 200 - except: - return {"error": True, "type": "Internal"}, 500 - -@app.route('/statistics', methods=["GET"]) -def get_statistics(): - try: - users = filesystem.count_items("usersv0", {}) - posts = filesystem.count_items("posts", {"isDeleted": False}) - chats = filesystem.count_items("chats", {}) - return {"error": False, "users": users, "posts": posts, "chats": chats}, 200 - except: - return {"error": True, "type": "Internal"}, 500 - -@app.route('/status', methods=["GET"]) -def get_status(): - result, payload = filesystem.load_item("config", "status") - if result: - return {"isRepairMode": payload["repair_mode"], "scratchDeprecated": payload["is_deprecated"]}, 200 - else: - return {"error": True, "type": "Internal"}, 500 - -@app.errorhandler(405) # Method not allowed -def not_allowed(e): - return {"error": True, "type": "methodNotAllowed"}, 405 - -@app.errorhandler(500) # Internal -def internal(e): - return {"error": True, "type": "Internal"}, 500 - -@app.errorhandler(404) # We do need a 404 handler. -def page_not_found(e): - return {"error": True, "type": "notFound"}, 404 diff --git a/run_api.py b/run_api.py new file mode 100644 index 0000000..ad73f40 --- /dev/null +++ b/run_api.py @@ -0,0 +1,11 @@ +from src.api.server import app +from src.common.util import config, display_startup + + +if __name__ == "__main__": + display_startup() + + try: + app.run(host=config.host, port=config.api_port, debug=config.development) + except KeyboardInterrupt: + exit() diff --git a/run_cl3.py b/run_cl3.py new file mode 100644 index 0000000..adcf617 --- /dev/null +++ b/run_cl3.py @@ -0,0 +1,10 @@ +import asyncio + +from src.cl3.server import cl +from src.common.util import config, display_startup + + +if __name__ == "__main__": + display_startup() + + asyncio.run(cl.main(host=config.host, port=config.cl3_port)) diff --git a/security.py b/security.py deleted file mode 100644 index b2e8315..0000000 --- a/security.py +++ /dev/null @@ -1,282 +0,0 @@ -import bcrypt -import time -from uuid import uuid4 - -""" -Meower Security Module -This module provides account management and authentication services. -""" - -class Security: - def __init__(self, files, supporter, logger, errorhandler): - self.bc = bcrypt - self.supporter = supporter - self.files = files - self.log = logger - self.errorhandler = errorhandler - self.log("Security initialized!") - - def create_account(self, username, password, strength=12): - - """ - Returns 2 booleans. - - | FileCheck | FileWrite | Definiton - |---------|----------|----------------- - | True | True | Account created - | True | False | Account creation error - | False | True | Account already exists - | False | False | Exception - """ - - if (type(password) == str) and (type(username) == str): - if not self.account_exists(str(username), ignore_case=True): - self.log("Creating account: {0}".format(username)) - pswd_bytes = bytes(password, "utf-8") # Convert password to bytes - hashed_pw = self.bc.hashpw(pswd_bytes, self.bc.gensalt(strength)) # Hash and salt the password - result = self.files.create_item("usersv0", str(username), { # Default account data - "lower_username": username.lower(), - "created": int(time.time()), - "uuid": str(uuid4()), - "unread_inbox": False, - "theme": "orange", - "mode": True, - "sfx": True, - "debug": False, - "bgm": True, - "bgm_song": 2, - "layout": "new", - "pfp_data": 1, - "quote": "", - "email": "", - "pswd": hashed_pw.decode(), - "tokens": [], - "lvl": 0, - "banned": False, - "last_ip": None - } - ) - return True, result - else: - self.log("Not creating account {0}: Account already exists".format(username)) - return False, True - else: - self.log("Error on generate_account: Expected str for username and password, got {0} for username and {1} for password".format(type(username), type(password))) - return False, False - - def get_account(self, username, omitSensitive=False, isClient=False): - """ - Returns 2 booleans, plus a payload. - - | FileCheck | FileRead | Definiton - |---------|----------|----------------- - | True | True | Account exists and read - | True | False | Account exists, read error - | False | True | Account does not exist - | False | False | Exception - """ - - if type(username) == str: - if self.files.does_item_exist("usersv0", str(username)): - self.log("Reading account: {0}".format(username)) - result, accountData = self.files.load_item("usersv0", str(username)) - - if omitSensitive: # Purge sensitive data and remove user settings - for sensitive in [ - "unread_inbox", - "theme", - "mode", - "sfx", - "debug", - "bgm", - "bgm_song", - "layout", - "email", - "pswd", - "tokens", - "last_ip" - ]: - if sensitive in accountData: - del accountData[sensitive] - - if isClient: - if "pswd" in accountData: - del accountData["pswd"] - if "tokens" in accountData: - del accountData["tokens"] - if "last_ip" in accountData: - del accountData["last_ip"] - - return True, result, accountData - else: - return False, True, None - else: - self.log("Error on get_account: Expected str for username, got {0}".format(type(username))) - return False, False, None - - def authenticate(self, username, password): - """ - Returns 3 booleans. - - | FileCheck | FileRead | ValidAuth | Definiton - |---------|----------|----------|----------------- - | True | True | True | Account exists, read OK, authentication valid - | True | True | False | Account exists, read OK, authentication invalid - | True | False | False | Account exists, read error - | False | True | False | Account does not exist - | False | False | False | Exception - """ - - if type(username) == str: - if self.files.does_item_exist("usersv0", str(username)): - self.log("Authenticating account: {0}".format(username)) - FileRead, accountData = self.files.load_item("usersv0", str(username)) - if FileRead: - if type(accountData) == dict: - if accountData["banned"] == True: - return True, True, False, True - if password in accountData["tokens"]: - self.log("Authenticating {0}: True".format(username)) - accountData["tokens"].remove(password) - self.update_setting(username, {"tokens": accountData["tokens"]}, forceUpdate=True) - return True, True, True, False - else: - hashed_pw = accountData["pswd"] - pswd_bytes = bytes(password, "utf-8") - hashed_pw_bytes = bytes(hashed_pw, "utf-8") - try: - result = self.bc.checkpw(pswd_bytes, hashed_pw_bytes) - self.log("Authenticating {0}: {1}".format(username, result)) - return True, True, result, False - except Exception as e: - self.log("Error on authenticate: {0}".format(e)) - return True, True, False, False - else: - self.log("Error on get_account: Expected str for username, got {0}".format(type(username))) - return False, False, False, False - else: - return True, False, False, False - else: - return False, True, False, False - else: - self.log("Error on get_account: Expected str for username, got {0}".format(type(username))) - return False, False, False, False - - def change_password(self, username, newpassword, strength=12): - """ - Returns 3 booleans. - - | FileCheck | FileRead | FileWrite | Definiton - |---------|----------|----------|----------------- - | True | True | True | Account exists, read OK, password changed - | True | False | False | Account exists, read error - | False | True | False | Account does not exist - | False | False | False | Exception - """ - - if (type(username) == str) and (type(newpassword) == str): - if self.files.does_item_exist("usersv0", str(username)): - self.log("Changing {0} password".format(username)) - result, accountData = self.files.load_item("usersv0", str(username)) - if result: - try: - pswd_bytes = bytes(newpassword, "utf-8") # Convert password to bytes - hashed_pw = self.bc.hashpw(pswd_bytes, self.bc.gensalt(strength)) # Hash and salt the password - - accountData["pswd"] = hashed_pw.decode() - - result = self.files.write_item("usersv0", str(username), accountData) - self.log("Change {0} password: {1}".format(username, result)) - return True, True, result - except Exception as e: - self.log("Error on authenticate: {0}".format(e)) - return True, True, False - else: - return True, False, False - else: - return False, True, False - else: - self.log("Error on get_account: Expected str for username, oldpassword and newpassword, got {0} for username and {1} for newpassword".format(type(username), type(newpassword))) - return False, False, False - - def account_exists(self, username, ignore_case=False): - if type(username) == str: - if ignore_case: - payload = self.files.find_items("usersv0", {"lower_username": str(username).lower()}) - if len(payload) == 0: - return False - else: - return True - else: - return self.files.does_item_exist("usersv0", str(username)) - else: - self.log("Error on account_exists: Expected str for username, got {0}".format(type(username))) - return False - - def is_account_banned(self, username): - """ - Returns 2 booleans, plus a payload. - - | FileCheck | FileRead | Banned | Definiton - |---------|----------|----------|----------------- - | True | True | True | Account exists and read, account banned - | True | True | False | Account exists and read, account NOT banned - | True | False | False | Account exists, read error - | False | True | False | Account does not exist - | False | False | False | Exception - """ - - if type(username) == str: - if self.files.does_item_exist("usersv0", str(username)): - self.log("Reading account: {0}".format(username)) - result, accountData = self.files.load_item("usersv0", str(username)) - return True, result, accountData["banned"] - else: - return False, True, None - else: - self.log("Error on get_account: Expected str for username, got {0}".format(type(username))) - return False, False, None - - def update_setting(self, username, newdata, forceUpdate=False): - """ - Returns 3 booleans. - - | FileCheck | FileRead | FileWrite | Definiton - |---------|----------|----------|----------------- - | True | True | True | Account exists, read OK, settings changed - | True | True | False | Account exists, read OK, settings write error - | True | False | False | Account exists, read error - | False | True | False | Account does not exist - | False | False | False | Exception - """ - - if (type(username) == str) and (type(newdata) == dict): - if self.files.does_item_exist("usersv0", str(username)): - self.log("Updating account settings: {0}".format(username)) - result, accountData = self.files.load_item("usersv0", str(username)) - if result: - for key, value in newdata.items(): - if key in accountData.keys(): - if forceUpdate: - accountData[key] = value - else: - if key not in ["lvl", "pswd", "banned", "email", "last_ip", "lower_username", "uuid", "tokens", "created"]: - if key in accountData.keys(): - if ((type(value) == str) and (len(value) <= 360)) or ((type(value) == int) and (len(str(value)) <= 360)) or ((type(value) == float) and (len(str(value)) <= 360)) or (type(value) == bool) or (type(value) == None): - if type(value) == str: - accountData[key] = self.supporter.wordfilter(value) - else: - accountData[key] = value - else: - self.log("Blocking attempt to modify secure key {0}".format(key)) - - result = self.files.write_item("usersv0", str(username), accountData) - self.log("Updating {0} account settings: {1}".format(username, result)) - return True, True, result - else: - return True, False, False - else: - return False, True, False - else: - self.log("Error on get_account: Expected str for username and dict for newdata, got {0} for username and {1} for newdata".format(type(username), type(newdata))) - return False, False, False diff --git a/src/api/server.py b/src/api/server.py new file mode 100644 index 0000000..08468ef --- /dev/null +++ b/src/api/server.py @@ -0,0 +1,305 @@ +from flask import Flask, request +from flask_cors import CORS + +from src.common.entities import users, posts, chats, reports, audit_log +from src.common.util import errors +from src.common.database import db, redis + + +app = Flask(__name__, static_folder="static") +cors = CORS(app, resources=r'*') + + +@app.before_request +def pre_request_check_auth(): + request.user = None + try: + if ("username" in request.headers) and ("token" in request.headers): + user = users.get_user(request.headers["username"]) + if (not user.banned) and (user.validate_token(request.headers["token"])): + request.user = user + except: + pass + + +@app.route("/") +def index(): + return "Hello world! The Meower API is working, but it's under construction. Please come back later." + + +@app.route("/ip") # deprecated +def ip_tracer(): + return "", 410 + + +@app.route("/favicon.ico") # Favicon, my ass. We need no favicon for an API. +def favicon_my_ass(): + return "", 204 + + +@app.route("/posts") +def get_post(): + # Check whether the post ID was specified + if "id" not in request.args: + return {"error": True, "type": "noQueryString"}, 400 + + # Get post + try: + post = posts.get_post(request.args["id"]) + except errors.NotFound: + return {"error": True, "type": "notFound"}, 404 + + # Check whether the user has access to the post + if not post.has_access(request.user): + return {"error": True, "type": "notFound"}, 404 + + # Return post + return post.public + + +@app.route("/posts/") +def get_chat_posts(chat_id): + # Get page + try: + page = int(request.args.get("page", 1)) + except ValueError: + return {"error": True, "type": "Datatype"}, 400 + + # Check whether the user is authenticated + if not request.user: + return {"error": True, "type": "Unauthorized"}, 401 + + # Get chat + try: + chat = chats.get_chat(chat_id) + except errors.NotFound: + return {"error": True, "type": "notFound"}, 404 + + # Check whether the user is in the chat + if request.user.username not in chat.members: + return {"error": True, "type": "notFound"}, 404 + + # Get posts + pages, fetched_posts = posts.get_posts(chat_id, page=page) + + # Return posts + return { + "error": False, + "autoget": [post.public for post in fetched_posts], + "page#": page, + "pages": pages + } + + +@app.route("/home") +def get_home(): + # Get page + try: + page = int(request.args.get("page", 1)) + except ValueError: + return {"error": True, "type": "Datatype"}, 400 + + # Check whether the user is authenticated + if (not request.user) and (page > 1): + return {"error": True, "type": "Unauthorized"}, 401 + + # Get posts + pages, fetched_posts = posts.get_posts("home", page=page) + + # Return posts + return { + "error": False, + "autoget": [post.public for post in fetched_posts], + "page#": page, + "pages": pages + } + + +@app.route("/reports") +def get_reports(): + # Get page + try: + page = int(request.args.get("page", 1)) + except ValueError: + return {"error": True, "type": "Datatype"}, 400 + + # Check whether the user is authenticated + if (not request.user) or (request.user.lvl < 1): + return {"error": True, "type": "Unauthorized"}, 401 + + # Get reports + pages, fetched_reports = reports.get_reports(page=page) + + # Return reports + return { + "error": False, + "autoget": [report.admin for report in fetched_reports], + "page#": page, + "pages": pages + } + + +@app.route("/logs") +def get_logs(): + # Get page + try: + page = int(request.args.get("page", 1)) + except ValueError: + return {"error": True, "type": "Datatype"}, 400 + + # Check whether the user is authenticated + if (not request.user) or (request.user.lvl < 2): + return {"error": True, "type": "Unauthorized"}, 401 + + # Get logs + pages, fetched_logs = audit_log.get_logs(page=page) + + # Return logs + return { + "error": False, + "autoget": [log.admin for log in fetched_logs], + "page#": page, + "pages": pages + } + + +@app.route("/search/home") +def search_home(): + # Check whether the query was specified + if "q" not in request.args: + return {"error": True, "type": "noQueryString"}, 400 + + # Get page + try: + page = int(request.args.get("page", 1)) + except ValueError: + return {"error": True, "type": "Datatype"}, 400 + + # Get posts + pages, fetched_posts = posts.search_posts("home", request.args["q"], page=page) + + # Return posts + return { + "error": False, + "autoget": [post.public for post in fetched_posts], + "page#": page, + "pages": pages + } + + +@app.route("/search/users") +def search_users(): + # Check whether the query was specified + if "q" not in request.args: + return {"error": True, "type": "noQueryString"}, 400 + + # Get page + try: + page = int(request.args.get("page", 1)) + except ValueError: + return {"error": True, "type": "Datatype"}, 400 + + # Get users + pages, fetched_users = users.search_users(request.args["q"], page=page) + + # Return users + return { + "error": False, + "autoget": [user.public for user in fetched_users], + "page#": page, + "pages": pages + } + + +@app.route("/inbox") +def get_inbox(): + # Get page + try: + page = int(request.args.get("page", 1)) + except ValueError: + return {"error": True, "type": "Datatype"}, 400 + + # Check whether the user is authenticated + if not request.user: + return {"error": True, "type": "Unauthorized"}, 401 + + # Get posts + pages, fetched_posts = posts.get_inbox_messages(request.user.username, page=page) + + # Return posts + return { + "error": False, + "autoget": [post.public for post in fetched_posts], + "page#": page, + "pages": pages + } + + +@app.route("/users/") +def get_user(username): + # Get user + try: + user = users.get_user(username) + except errors.NotFound: + return {"error": True, "type": "notFound"}, 404 + + # Return user + return user.public + + +@app.route("/users//posts") +def get_user_posts(username): + # Get page + try: + page = int(request.args.get("page", 1)) + except ValueError: + return {"error": True, "type": "Datatype"}, 400 + + # Check if user exists + if not users.username_exists(username): + return {"error": True, "type": "notFound"}, 404 + + # Get posts + pages, fetched_posts = posts.get_posts("home", author=username, page=page) + + # Return posts + return { + "error": False, + "autoget": [post.public for post in fetched_posts], + "page#": page, + "pages": pages + } + + +@app.route("/statistics") +def get_statistics(): + return { + "error": False, + "users": db.users.estimated_document_count(), + "posts": db.posts.estimated_document_count(), + "chats": db.chats.estimated_document_count() + } + + +@app.route("/status") +def get_status(): + return { + "error": False, + "isRepairMode": (redis.exists("repair_mode") == 1), + "scratchDeprecated": False + } + + +@app.errorhandler(404) # We do need a 404 handler. +def page_not_found(e): + return {"error": True, "type": "notFound"}, 404 + + +@app.errorhandler(405) # Method not allowed +def not_allowed(e): + return {"error": True, "type": "methodNotAllowed"}, 405 + + +@app.errorhandler(500) # Internal +def internal(e): + return {"error": True, "type": "Internal"}, 500 diff --git a/static/bgm/1.mp3 b/src/api/static/bgm/1.mp3 similarity index 100% rename from static/bgm/1.mp3 rename to src/api/static/bgm/1.mp3 diff --git a/static/bgm/10.mp3 b/src/api/static/bgm/10.mp3 similarity index 100% rename from static/bgm/10.mp3 rename to src/api/static/bgm/10.mp3 diff --git a/static/bgm/2.mp3 b/src/api/static/bgm/2.mp3 similarity index 100% rename from static/bgm/2.mp3 rename to src/api/static/bgm/2.mp3 diff --git a/static/bgm/3.mp3 b/src/api/static/bgm/3.mp3 similarity index 100% rename from static/bgm/3.mp3 rename to src/api/static/bgm/3.mp3 diff --git a/static/bgm/4.mp3 b/src/api/static/bgm/4.mp3 similarity index 100% rename from static/bgm/4.mp3 rename to src/api/static/bgm/4.mp3 diff --git a/static/bgm/5.mp3 b/src/api/static/bgm/5.mp3 similarity index 100% rename from static/bgm/5.mp3 rename to src/api/static/bgm/5.mp3 diff --git a/static/bgm/6.mp3 b/src/api/static/bgm/6.mp3 similarity index 100% rename from static/bgm/6.mp3 rename to src/api/static/bgm/6.mp3 diff --git a/static/bgm/7.mp3 b/src/api/static/bgm/7.mp3 similarity index 100% rename from static/bgm/7.mp3 rename to src/api/static/bgm/7.mp3 diff --git a/static/bgm/8.mp3 b/src/api/static/bgm/8.mp3 similarity index 100% rename from static/bgm/8.mp3 rename to src/api/static/bgm/8.mp3 diff --git a/static/bgm/9.mp3 b/src/api/static/bgm/9.mp3 similarity index 100% rename from static/bgm/9.mp3 rename to src/api/static/bgm/9.mp3 diff --git a/static/icon/-1.svg b/src/api/static/icon/-1.svg similarity index 100% rename from static/icon/-1.svg rename to src/api/static/icon/-1.svg diff --git a/static/icon/0.svg b/src/api/static/icon/0.svg similarity index 100% rename from static/icon/0.svg rename to src/api/static/icon/0.svg diff --git a/static/icon/1.svg b/src/api/static/icon/1.svg similarity index 100% rename from static/icon/1.svg rename to src/api/static/icon/1.svg diff --git a/static/icon/10.svg b/src/api/static/icon/10.svg similarity index 100% rename from static/icon/10.svg rename to src/api/static/icon/10.svg diff --git a/static/icon/11.svg b/src/api/static/icon/11.svg similarity index 100% rename from static/icon/11.svg rename to src/api/static/icon/11.svg diff --git a/static/icon/12.svg b/src/api/static/icon/12.svg similarity index 100% rename from static/icon/12.svg rename to src/api/static/icon/12.svg diff --git a/static/icon/13.svg b/src/api/static/icon/13.svg similarity index 100% rename from static/icon/13.svg rename to src/api/static/icon/13.svg diff --git a/static/icon/14.svg b/src/api/static/icon/14.svg similarity index 100% rename from static/icon/14.svg rename to src/api/static/icon/14.svg diff --git a/static/icon/15.svg b/src/api/static/icon/15.svg similarity index 100% rename from static/icon/15.svg rename to src/api/static/icon/15.svg diff --git a/static/icon/16.svg b/src/api/static/icon/16.svg similarity index 100% rename from static/icon/16.svg rename to src/api/static/icon/16.svg diff --git a/static/icon/17.svg b/src/api/static/icon/17.svg similarity index 100% rename from static/icon/17.svg rename to src/api/static/icon/17.svg diff --git a/static/icon/18.svg b/src/api/static/icon/18.svg similarity index 100% rename from static/icon/18.svg rename to src/api/static/icon/18.svg diff --git a/static/icon/19.svg b/src/api/static/icon/19.svg similarity index 100% rename from static/icon/19.svg rename to src/api/static/icon/19.svg diff --git a/static/icon/2.svg b/src/api/static/icon/2.svg similarity index 100% rename from static/icon/2.svg rename to src/api/static/icon/2.svg diff --git a/static/icon/20.svg b/src/api/static/icon/20.svg similarity index 100% rename from static/icon/20.svg rename to src/api/static/icon/20.svg diff --git a/static/icon/21.svg b/src/api/static/icon/21.svg similarity index 100% rename from static/icon/21.svg rename to src/api/static/icon/21.svg diff --git a/static/icon/22.svg b/src/api/static/icon/22.svg similarity index 100% rename from static/icon/22.svg rename to src/api/static/icon/22.svg diff --git a/static/icon/23.svg b/src/api/static/icon/23.svg similarity index 100% rename from static/icon/23.svg rename to src/api/static/icon/23.svg diff --git a/static/icon/24.svg b/src/api/static/icon/24.svg similarity index 100% rename from static/icon/24.svg rename to src/api/static/icon/24.svg diff --git a/static/icon/25.svg b/src/api/static/icon/25.svg similarity index 100% rename from static/icon/25.svg rename to src/api/static/icon/25.svg diff --git a/static/icon/26.svg b/src/api/static/icon/26.svg similarity index 100% rename from static/icon/26.svg rename to src/api/static/icon/26.svg diff --git a/static/icon/27.svg b/src/api/static/icon/27.svg similarity index 100% rename from static/icon/27.svg rename to src/api/static/icon/27.svg diff --git a/static/icon/3.svg b/src/api/static/icon/3.svg similarity index 100% rename from static/icon/3.svg rename to src/api/static/icon/3.svg diff --git a/static/icon/4.svg b/src/api/static/icon/4.svg similarity index 100% rename from static/icon/4.svg rename to src/api/static/icon/4.svg diff --git a/static/icon/5.svg b/src/api/static/icon/5.svg similarity index 100% rename from static/icon/5.svg rename to src/api/static/icon/5.svg diff --git a/static/icon/6.svg b/src/api/static/icon/6.svg similarity index 100% rename from static/icon/6.svg rename to src/api/static/icon/6.svg diff --git a/static/icon/7.svg b/src/api/static/icon/7.svg similarity index 100% rename from static/icon/7.svg rename to src/api/static/icon/7.svg diff --git a/static/icon/8.svg b/src/api/static/icon/8.svg similarity index 100% rename from static/icon/8.svg rename to src/api/static/icon/8.svg diff --git a/static/icon/9.svg b/src/api/static/icon/9.svg similarity index 100% rename from static/icon/9.svg rename to src/api/static/icon/9.svg diff --git a/static/icon/err.svg b/src/api/static/icon/err.svg similarity index 100% rename from static/icon/err.svg rename to src/api/static/icon/err.svg diff --git a/static/icon/load_0.svg b/src/api/static/icon/load_0.svg similarity index 100% rename from static/icon/load_0.svg rename to src/api/static/icon/load_0.svg diff --git a/static/icon/load_1.svg b/src/api/static/icon/load_1.svg similarity index 100% rename from static/icon/load_1.svg rename to src/api/static/icon/load_1.svg diff --git a/static/icon/load_2.svg b/src/api/static/icon/load_2.svg similarity index 100% rename from static/icon/load_2.svg rename to src/api/static/icon/load_2.svg diff --git a/static/icon/load_3.svg b/src/api/static/icon/load_3.svg similarity index 100% rename from static/icon/load_3.svg rename to src/api/static/icon/load_3.svg diff --git a/static/icon/load_4.svg b/src/api/static/icon/load_4.svg similarity index 100% rename from static/icon/load_4.svg rename to src/api/static/icon/load_4.svg diff --git a/static/icon/load_5.svg b/src/api/static/icon/load_5.svg similarity index 100% rename from static/icon/load_5.svg rename to src/api/static/icon/load_5.svg diff --git a/static/icon/load_6.svg b/src/api/static/icon/load_6.svg similarity index 100% rename from static/icon/load_6.svg rename to src/api/static/icon/load_6.svg diff --git a/static/icon/load_7.svg b/src/api/static/icon/load_7.svg similarity index 100% rename from static/icon/load_7.svg rename to src/api/static/icon/load_7.svg diff --git a/src/cl3/commands.py b/src/cl3/commands.py new file mode 100644 index 0000000..9daf2da --- /dev/null +++ b/src/cl3/commands.py @@ -0,0 +1,1212 @@ +from src.common.util import config, validate, errors, security, events +from src.common.entities import users, networks, posts, chats, reports, audit_log + + +class CL3Commands: + def __init__(self, cl_server): + self.cl = cl_server + + # CL3 commands + async def ip(self, client, val, listener): pass # deprecated + + async def type(self, client, val, listener): pass # deprecated + + async def direct(self, client, val, listener): pass # deprecated + + async def setid(self, client, val, listener): pass # deprecated + + async def pmsg(self, client, val, listener): + # Check if the client is already authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"id": (str, None, 20), "val": (None, None, 360)}) + + # Ratelimit client + security.auto_ratelimit(client.username, "pmsg", 10, 5) + + # Send pmsg event + events.send_event("pmsg", { + "username": val["id"], + "origin": client.username, + "val": val["val"] + }) + + async def pvar(self, client, val, listener): + # Check if the client is already authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"id": (str, None, 20), "name": (None, None, 360), "val": (None, None, 360)}) + + # Ratelimit client + security.auto_ratelimit(client.username, "pvar", 10, 5) + + # Send pvar event + events.send_event("pvar", { + "username": val["id"], + "origin": client.username, + "name": val["name"], + "val": val["val"] + }) + + # Networking/client utilities + + async def ping(self, client, val, listener): pass + + async def version_chk(self, client, val, listener): pass # deprecated + + async def get_ulist(self, client, val, listener): + await self.cl.send_to_client(client, {"cmd": "ulist", "val": self.cl.ulist}, listener) + + async def get_peak_users(self, client, val, listener): + await self.cl.send_to_client(client, { + "cmd": "direct", + "val": { + "mode": "peak", + "payload": self.cl.peak_users + } + }, listener) + + # Accounts and security + + async def authpswd(self, client, val, listener): + # Check if the client is already authenticated + if client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"username": (str, None, 20), "pswd": (str, None, 255)}) + + # Extract username and password + username = val["username"] + password = val["pswd"] + + # Check whether client is ratelimited + if security.check_ratelimit(client.ip, "login"): + raise errors.Ratelimited + + # Get user + user = users.get_user(username) + + # Check whether user is banned + if user.banned: + raise errors.UserBanned + + # Check token/password and get new token + if user.validate_token(password): + token = password + elif user.check_password(password): + token = user.generate_token() + else: + security.ratelimit(client.ip, "login", 5, 60) + raise errors.InvalidPassword + + # Log current network + network = networks.get_network(client.ip) + network.log_user(user.username) + + # Cancel scheduled account deletion + if user.delete_after: + user.cancel_scheduled_deletion() + + # Authenticate client + client.username = user.username + if user.invisible: + self.cl.invisible_users.add(user.username) + if client.username not in self.cl.users: + self.cl.users[client.username] = set() + self.cl.users[client.username].add(client) + + # Subscribe to chats + for chat_id in chats.get_all_chat_ids(client.username): + self.cl.subscribe_to_chat(client, chat_id) + + # Return payload to client + payload = { + "cmd": "direct", + "val": { + "mode": "auth", + "payload": { + "username": username, + "token": token + } + } + } + await self.cl.broadcast({"cmd": "ulist", "val": self.cl.ulist}) + await self.cl.send_to_client(client, payload, listener) + await self.cl.log_peak_users() + + async def gen_account(self, client, val, listener): + # Check if the client is already authenticated + if client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"username": (str, 1, 20), "pswd": (str, 8, 255)}) + + # Extract username and password + username = val["username"] + password = val["pswd"] + + # Check whether client is ratelimited + if security.check_ratelimit(client.ip, "register"): + raise errors.Ratelimited + + # Check whether network is a proxy + network = networks.get_network(client.ip) + if config.block_proxies and network.proxy: + raise errors.IPBanned + + # Create user account + user = users.create_user(username, password) + + # Ratelimit client + security.ratelimit(client.ip, "register", 2, 120) + + # Generate token + token = user.generate_token() + + # Log current network + network.log_user(user.username) + + # Authenticate client + client.username = user.username + if user.invisible: + self.cl.invisible_users.add(user.username) + if client.username not in self.cl.users: + self.cl.users[client.username] = set() + self.cl.users[client.username].add(client) + + # Subscribe to chats + for chat_id in chats.get_all_chat_ids(client.username): + self.cl.subscribe_to_chat(client, chat_id) + + # Return payload to client + payload = { + "cmd": "direct", + "val": { + "mode": "auth", + "payload": { + "username": username, + "token": token + } + } + } + await self.cl.broadcast({"cmd": "ulist", "val": self.cl.ulist}) + await self.cl.send_to_client(client, payload, listener) + await self.cl.log_peak_users() + + async def get_profile(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 20)}) + + # Get user + user = users.get_user(val) + + # Return profile + payload = { + "cmd": "direct", + "val": { + "mode": "profile", + "payload": (user.client if (client.username == user.username) else user.public), + "user_id": user.username + } + } + await self.cl.send_to_client(client, payload, listener) + + async def update_config(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Check datatype + if not isinstance(val, dict): + raise errors.InvalidDatatype + + # Get user + user = users.get_user(client.username) + + # Update the config + user.update_config(val) + + async def change_pswd(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"old": (str, None, 255), "new": (str, 8, 255)}) + + # Check whether client is ratelimited + if security.check_ratelimit(client.username, "login"): + raise errors.Ratelimited + + # Get user + user = users.get_user(client.username) + + # Check old password + if not user.check_password(val["old"]): + security.ratelimit(client.username, "login", 5, 60) + raise errors.InvalidPassword + + # Set new password + user.change_password(val["new"]) + + async def del_tokens(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Get user + user = users.get_user(client.username) + + # Revoke sessions + user.revoke_sessions() + + async def del_account(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 255)}) + + # Get user + user = users.get_user(client.username) + + # Check password + if not user.check_password(val): + raise errors.InvalidPassword + + # Schedule account for deletion + user.schedule_deletion() + + # General + + async def get_inbox(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.MissingPermissions + + # Validate payload + validate(val, {"page": (int, 1, None)}, optional=["page"]) + + # Extract page + if isinstance(val, dict): + page = val.get("page", 1) + else: + page = 1 + + # Get inbox index + pages, fetched_posts = posts.get_inbox_messages(client.username, page=page) + + # Return inbox index + payload = { + "cmd": "direct", + "val": { + "mode": "inbox", + "payload": { + "index": [post.id for post in fetched_posts], + "page#": page, + "pages": pages, + "query": { + "origin": "inbox", + "deleted_at": None, + "author": {"$in": [client.username, "Server"]} + } + } + } + } + await self.cl.send_to_client(client, payload, listener) + + async def get_home(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"page": (int, 1, None)}, optional=["page"]) + + # Extract page + if isinstance(val, dict): + page = val.get("page", 1) + else: + page = 1 + + # Get home posts + pages, fetched_posts = posts.get_posts("home", page=page) + + # Return home index + payload = { + "cmd": "direct", + "val": { + "mode": "home", + "payload": { + "index": [post.id for post in fetched_posts], + "page#": page, + "pages": pages, + "query": { + "origin": "home", + "deleted_at": None + } + } + } + } + await self.cl.send_to_client(client, payload, listener) + + async def post_home(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, 1, 4000)}) + + # Ratelimit client + security.auto_ratelimit(client.username, "post", 5, 5) + + # Create post + posts.create_post("home", client.username, val) + + async def get_post(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 36)}) + + # Get post + post = posts.get_post(val) + + # Check if user has permission to access the post + if not post.has_access(client.username): + raise errors.NotFound + + # Return post + payload = { + "cmd": "direct", + "val": { + "mode": "post", + "payload": post.public + } + } + await self.cl.send_to_client(client, payload, listener) + + async def delete_post(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 36)}) + + # Get post + post = posts.get_post(val) + + # Get user + user = users.get_user(client.username) + + # Check whether the client can delete the post + if (client.username != post.author) and (user.lvl < 1): + raise errors.MissingPermissions + + # Delete the post + post.delete(client.username) + + # Logging and data management + + async def search_user_posts(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"query": (str, None, 20), "page": (int, 1, None)}, optional=["page"]) + + # Extract username and page + username = val["query"] + page = val.get("page", 1) + + # Make sure user exists + if not users.username_exists(username): + raise errors.NotFound + + # Get user posts + pages, fetched_posts = posts.get_posts("home", author=username, page=page) + + # Return user posts index + payload = { + "cmd": "direct", + "val": { + "mode": "user_posts", + "index": { + "index": [post.id for post in fetched_posts], + "page#": page, + "pages": pages, + "query": { + "origin": "home", + "deleted_at": None, + "author": username + } + } + } + } + await self.cl.send_to_client(client, payload, listener) + + # Moderator features + + async def report(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"type": (int, 0, 1), "id": (str, None, 36)}) + + # Get user + user = users.get_user(client.username) + + # Create report + reports.create_report(val["type"], val["id"], user.username, user.report_reputation) + + async def close_report(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 36)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 1: + raise errors.MissingPermissions + + # Get report + report = reports.get_report(val) + + # Close report + report.close(False) + + async def clear_home(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"page": (int, 1, None)}, optional=["page"]) + + # Extract page + page = val.get("page", 1) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 1: + raise errors.MissingPermissions + + # Get home page + pages, fetched_posts = posts.get_posts("home", page=page) + + # Delete all fetched posts + for post in fetched_posts: + post.delete(client.username) + + async def clear_user_posts(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 20)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 1: + raise errors.MissingPermissions + + # Get user + user = users.get_user(val) + + # Clear user's posts + user.clear_posts(moderator=client.username) + + async def get_user_data(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 20)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 1: + raise errors.MissingPermissions + + # Get user + user = users.get_user(val) + + # Return user data + payload = { + "cmd": "direct", + "val": { + "mode": "user_data", + "payload": user.client + } + } + await self.cl.send_to_client(client, payload, listener) + + # Create audit log item + audit_log.create_log("get_user_data", client.username, { + "username": val + }) + + async def alert(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"username": (str, None, 20), "p": (str, 1, 4000)}) + + # Extract username and content + username = val["username"] + content = val["p"] + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 1: + raise errors.MissingPermissions + + # Get user + user = users.get_user(username) + + # Alert user + user.alert(content, moderator=client.username) + + async def kick(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 20)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 1: + raise errors.MissingPermissions + + # Get user + user = users.get_user(val) + + # Kick user + user.kick(moderator=client.username) + + async def ban(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + if isinstance(val, str): + validate({"val": val}, {"val": (str, None, 20)}) + else: + validate(val, {"username": (str, None, 20), "expires": (int, None, None)}) + + # Extract username and expires + if isinstance(val, str): + username = val + expires = -1 + else: + username = val["username"] + expires = val["expires"] + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 1: + raise errors.MissingPermissions + + # Get user + user = users.get_user(username) + + # Ban user + user.ban(expires, moderator=client.username) + + async def pardon(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 20)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 1: + raise errors.MissingPermissions + + # Get user + user = users.get_user(val) + + # Unban user + user.unban() + + async def terminate(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 20)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 3: + raise errors.MissingPermissions + + # Get user + user = users.get_user(val) + + # Terminate user + user.terminate(moderator=client.username) + + async def gdpr(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 20)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 4: + raise errors.MissingPermissions + + # Get user + user = users.get_user(val) + + # Schedule user for deletion + user.schedule_deletion(delay=0) + + async def get_ip_data(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, 1, 64)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 2: + raise errors.MissingPermissions + + # Get network + network = networks.get_network(val) + + # Return network data + payload = { + "cmd": "direct", + "val": { + "mode": "ip_data", + "payload": network.admin + } + } + await self.cl.send_to_client(client, payload, listener) + + # Create audit log item + audit_log.create_log("get_ip_data", client.username, { + "ip": val + }) + + async def block(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, 1, 64)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 2: + raise errors.MissingPermissions + + # Get network + network = networks.get_network(val) + + # Ban network + network.set_ban_state(True) + + async def unblock(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, 1, 64)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 2: + raise errors.MissingPermissions + + # Get network + network = networks.get_network(val) + + # Unban network + network.set_ban_state(False) + + async def get_user_ip(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 20)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 2: + raise errors.MissingPermissions + + # Get user + user = users.get_user(val) + + # Return user IP + payload = { + "cmd": "direct", + "val": { + "mode": "user_ip", + "payload": { + "username": user.username, + "ip": user.last_ip + } + } + } + await self.cl.send_to_client(client, payload, listener) + + async def ip_ban(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, 1, 64)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 2: + raise errors.MissingPermissions + + # Get user + user = users.get_user(val) + + # Get network + network = networks.get_network(user.last_ip) + + # Ban network + network.set_ban_state(True) + + async def ip_pardon(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, 1, 64)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 2: + raise errors.MissingPermissions + + # Get user + user = users.get_user(val) + + # Get network + network = networks.get_network(user.last_ip) + + # Unban network + network.set_ban_state(False) + + async def announce(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, 1, 4000)}) + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 3: + raise errors.MissingPermissions + + # Create announcement + posts.create_announcement(val) + + # Create audit log item + audit_log.create_log("create_announcement", client.username, { + "content": val + }) + + async def repair_mode(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Get user + user = users.get_user(client.username) + + # Check user level + if user.lvl < 4: + raise errors.MissingPermissions + + # Enable repair mode + events.send_event("update_server", {"repair_mode": True}) + + # Create audit log item + audit_log.create_log("enable_repair_mode", client.username, {}) + + # Chat-related + + async def get_chat_list(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"page": (int, 1, None)}, optional=["page"]) + + # Extract page + if isinstance(val, dict): + page = val.get("page", 1) + else: + page = 1 + + # Get chats + pages, user_chats = chats.get_users_chats(client.username, page=page) + + # Return chats index + payload = { + "cmd": "direct", + "val": { + "mode": "chats", + "payload": { + "index": [chat.id for chat in user_chats], + "all_chats": [chat.public for chat in user_chats], + "page#": page, + "pages": pages, + "query": { + "members": { + "$all": [client.username] + } + } + } + } + } + await self.cl.send_to_client(client, payload, listener) + + async def create_chat(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, 1, 20)}) + + # Ratelimit client + security.auto_ratelimit(client.username, "chat", 5, 3) + + # Create chat + chats.create_chat(val, client.username) + + async def join_chat(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, 7, 7)}) + + # Get chat + chat = chats.get_chat_by_invite_code(val) + + # Ratelimit client + security.auto_ratelimit(client.username, "chat", 5, 3) + + # Add client to the chat + try: + chat.add_member(client.username, client.username) + except errors.AlreadyExists: + pass + + async def leave_chat(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 36)}) + + # Get chat + chat = chats.get_chat(val) + + # Check if the client is in the chat + if client.username not in chat.members: + raise errors.NotFound + + # Remove member from the chat + chat.remove_member(client.username, client.username) + + async def get_chat_data(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 36)}) + + # Get chat + chat = chats.get_chat(val) + + # Check if the client is in the chat + if client.username not in chat.members: + raise errors.NotFound + + # Return chat data + chat_json = chat.public + chat_json["chatid"] = chat_json.pop("_id") + payload = { + "cmd": "direct", + "val": { + "mode": "chat_data", + "payload": chat_json + } + } + await self.cl.send_to_client(client, payload, listener) + + async def edit_chat(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"chatid": (str, None, 36), "nickname": (str, 1, 20), "owner": (str, None, 20), "invite_code": (bool, None, None)}, optional=["nickname", "owner", "invite_code"]) + + # Ratelimit client + security.auto_ratelimit(client.username, "chat", 5, 3) + + # Get chat + chat = chats.get_chat(val["chatid"]) + + # Check if the client is in the chat + if client.username not in chat.members: + raise errors.NotFound + + # Check if the client is the owner of the chat + if client.username != chat.owner: + raise errors.MissingPermissions + + # Change nickname + if val.get("nickname"): + chat.change_nickname(val["nickname"]) + + # Transfer ownership + if val.get("owner"): + chat.change_owner(val["owner"], actor=client.username) + + # Reset invite code + if val.get("invite_code"): + chat.reset_invite_code() + + async def delete_chat(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 36)}) + + # Get chat + chat = chats.get_chat(val) + + # Check if the client is in the chat + if client.username not in chat.members: + raise errors.NotFound + + # Check if the client is the owner of the chat + if client.username != chat.owner: + raise errors.MissingPermissions + + # Delete chat + chat.delete() + + async def add_to_chat(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"chatid": (str, None, 36), "username": (str, None, 20)}) + + # Ratelimit client + security.auto_ratelimit(client.username, "chat", 5, 3) + + # Get chat + chat = chats.get_chat(val["chatid"]) + + # Check if the client is in the chat + if client.username not in chat.members: + raise errors.NotFound + + # Add member to chat + chat.add_member(val["username"], client.username) + + async def remove_from_chat(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"chatid": (str, None, 36), "username": (str, None, 20)}) + + # Ratelimit client + security.auto_ratelimit(client.username, "chat", 5, 3) + + # Get chat + chat = chats.get_chat(val["chatid"]) + + # Check if the client is in the chat + if client.username not in chat.members: + raise errors.NotFound + + # Check if the client is the owner of the chat + if client.username != chat.owner: + raise errors.MissingPermissions + + # Remove member from chat + chat.remove_member(val["username"], client.username) + + async def set_chat_state(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"chatid": (str, None, 36), "state": (int, None, None)}) + + # Ratelimit client + security.auto_ratelimit(client.username, "chat", 5, 3) + + # Get chat + chat = chats.get_chat(val["chatid"]) + + # Check if the client is in the chat + if (chat.id != "livechat") and (client.username not in chat.members): + raise errors.NotFound + + # Broadcast new chat state + chat.set_chat_state(client.username, val["state"]) + + async def get_chat_posts(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate({"val": val}, {"val": (str, None, 36)}) + + # Get chat + chat = chats.get_chat(val) + + # Check if the client is in the chat + if client.username not in chat.members: + raise errors.NotFound + + # Get posts index + pages, fetched_posts = posts.get_posts(chat.id, page=1) + + # Return posts index + payload = { + "cmd": "direct", + "val": { + "mode": "chat_posts", + "payload": { + "index": [post.id for post in fetched_posts], + "page#": 1, + "pages": pages, + "query": { + "origin": chat.id, + "deleted_at": None + } + } + } + } + await self.cl.send_to_client(client, payload, listener) + + async def post_chat(self, client, val, listener): + # Check if the client is authenticated + if not client.username: + raise errors.NotAuthenticated + + # Validate payload + validate(val, {"chatid": (str, None, 36), "p": (str, 1, 4000)}) + + # Ratelimit client + security.auto_ratelimit(client.username, "post", 5, 5) + + # Get chat + chat = chats.get_chat(val["chatid"]) + + # Check if the client is in the chat + if (chat.id != "livechat") and (client.username not in chat.members): + raise errors.NotFound + + # Create post + posts.create_post(chat.id, client.username, val["p"]) diff --git a/src/cl3/events.py b/src/cl3/events.py new file mode 100644 index 0000000..72f7222 --- /dev/null +++ b/src/cl3/events.py @@ -0,0 +1,166 @@ +from copy import copy + +from src.common.util import events +from src.common.database import redis + + +class CL3Events: + def __init__(self, cl_server): + self.cl = cl_server + + events.add_event_listener(self.__event_handler__) + + async def __event_handler__(self, event: str, payload: dict): + if event == "pmsg": + await self.cl.send_to_user(payload["username"], { + "cmd": "pmsg", + "origin": payload["origin"], + "val": payload["val"] + }) + + elif event == "pvar": + await self.cl.send_to_user(payload["username"], { + "cmd": "pvar", + "origin": payload["origin"], + "name": payload["name"], + "val": payload["val"] + }) + + elif event == "kick_user": + for client in copy(self.cl.users.get(payload["username"], set())): + try: + await self.cl.kick_client(client, code=payload.get("code")) + except: + pass + + elif event == "kick_network": + for client in copy(self.cl.clients): + if client.ip != payload["ip"]: + continue + try: + await self.cl.kick_client(client, code=payload.get("code")) + except: + pass + + elif event == "update_server": + if payload.get("repair_mode", False): + redis.set("repair_mode", "") + for client in self.cl.clients: + try: + await self.cl.kick_client(client) + except: + pass + + elif event == "update_config": + # Extract username + username = payload["_id"] + + # Check if user is banned + if payload["banned"]: + for client in self.cl.users.get(username, set()): + await self.cl.kick_client(client, code="Banned") + + # Set invisible state + if payload["invisible"] and (username not in self.cl.invisible_users): + self.cl.invisible_users.add(username) + await self.cl.broadcast({"cmd": "ulist", "val": self.cl.ulist}) + elif (not payload["invisible"]) and (username in self.cl.invisible_users): + self.cl.invisible_users.remove(username) + await self.cl.broadcast({"cmd": "ulist", "val": self.cl.ulist}) + + # Announce update to other clients + await self.cl.send_to_user(username, { + "cmd": "direct", + "val": { + "mode": "update_config", + "payload": payload + } + }) + + elif event == "create_post": + if payload["post_origin"] == "home": + _payload = { + "cmd": "direct", + "val": payload + } + _payload["val"]["mode"] = 1 + await self.cl.broadcast(_payload) + elif payload["post_origin"] == "inbox": + _payload = { + "cmd": "direct", + "val": { + "mode": "inbox_message", + "payload": payload + } + } + if payload["u"] == "Server": + await self.cl.broadcast(_payload) + else: + await self.cl.send_to_user(payload["u"], _payload) + else: + _payload = { + "cmd": "direct", + "val": payload + } + _payload["val"]["state"] = 2 + await self.cl.send_to_chat(payload["post_origin"], _payload) + + elif event == "update_post": + if payload["isDeleted"]: + _payload = { + "cmd": "direct", + "val": { + "mode": "delete", + "id": payload["_id"] + } + } + else: + _payload = { + "cmd": "direct", + "val": payload + } + _payload["val"]["mode"] = "update_post" + + # Relay post update + if payload["post_origin"] == "home": + await self.cl.broadcast(_payload) + elif payload["post_origin"] == "inbox": + if payload["u"] == "Server": + await self.cl.broadcast(_payload) + else: + await self.cl.send_to_user(payload["u"], _payload) + else: + await self.cl.send_to_chat(payload["post_origin"], _payload) + + elif event == "update_chat": + # Add/remove members from chat event subscription + chat_id = payload["_id"] + members = set(payload["members"]) + for client in copy(self.cl.chats.get(chat_id, set())): + if client.username not in members: + self.cl.unsubscribe_from_chat(client, chat_id) + await self.cl.send_to_client(client, { + "cmd": "direct", + "val": { + "mode": "delete", + "id": chat_id + } + }) + for username in members: + for client in copy(self.cl.users.get(username, set())): + self.cl.subscribe_to_chat(client, chat_id) + + # Send update event + await self.cl.send_to_chat(chat_id, { + "cmd": "direct", + "val": { + "mode": "update_chat", + "payload": payload + } + }) + + elif event == "update_chat_state": + await self.cl.send_to_chat(payload["chatid"], { + "cmd": "direct", + "val": payload + }) diff --git a/src/cl3/server.py b/src/cl3/server.py new file mode 100644 index 0000000..070ad72 --- /dev/null +++ b/src/cl3/server.py @@ -0,0 +1,302 @@ +from copy import copy +import websockets +import asyncio +import ujson + +from src.cl3.events import CL3Events +from src.cl3.commands import CL3Commands +from src.common.entities import networks +from src.common.util import config, full_stack, uid +from src.common.database import redis + + +VERSION = "0.1.7.7" +CODES = { + "InvalidPassword": "I:011 | Invalid Password", + "IDExists": "I:015 | Account exists", + "2FAOnly": "I:016 | 2FA Required", + "MissingPermissions": "I:017 | Missing permissions", + "Banned": "E:018 | Account Banned", + "IllegalChars": "E:019 | Illegal characters detected", + "Kicked": "E:020 | Kicked", + "OK": "I:100 | OK", + "Syntax": "E:101 | Syntax", + "Datatype": "E:102 | Datatype", + "IDNotFound": "E:103 | ID not found", + "Internal": "E:104 | Internal", + "Loop": "E:105 | Loop detected", + "Ratelimited": "E:106 | Too many requests", + "TooLarge": "E:107 | Packet too large", + "IDRequired": "E:116 | Username required", + "Invalid": "E:118 | Invalid command", + "IPBanned": "E:119 | IP Blocked", + "Disabled": "E:122 | Command disabled by sysadmin", + "IllegalIP": "E:123 | Illegal IP" +} +COMMANDS = { + "ip", + "type", + "direct", + "setid", + "pmsg", + "pvar", + "ping", + "version_chk", + "get_ulist", + "get_peak_users", + "authpswd", + "gen_account", + "get_profile", + "update_config", + "change_pswd", + "del_tokens", + "del_account", + "get_inbox", + "get_home", + "post_home", + "get_post", + "delete_post", + "search_user_posts", + "report", + "close_report", + "clear_home", + "clear_user_posts", + "get_user_data", + "alert", + "kick", + "ban", + "pardon", + "terminate", + "gdpr", + "get_ip_data", + "block", + "unblock", + "get_user_ip", + "ip_ban", + "ip_pardon", + "announce", + "repair_mode", + "get_chat_list", + "create_chat", + "join_chat", + "leave_chat", + "get_chat_data", + "edit_chat", + "delete_chat", + "add_to_chat", + "remove_from_chat", + "set_chat_state", + "get_chat_posts", + "post_chat" +} +DISABLED_COMMANDS = { + "gmsg", + "gvar" +} + + +class server: + def __init__(self): + self.clients = set() + self.users = {} + self.invisible_users = set() + self.chats = {} + self.peak_users = {} + + self.events_handler = CL3Events(self) + self.command_handler = CL3Commands(self) + + @property + def ulist(self): + _ulist = "" + for username in self.users.keys(): + if username not in self.invisible_users: + _ulist += f"{username};" + return _ulist + + async def broadcast(self, payload: dict): + payload = ujson.dumps(payload) + for client in copy(self.clients): + try: + await client.send(payload) + except: + pass + + async def send_to_client(self, client, payload: dict, listener: str = None): + if listener: + payload["listener"] = listener + try: + await client.send(ujson.dumps(payload)) + except: + pass + + async def send_to_user(self, username: str, payload: dict): + payload = ujson.dumps(payload) + for client in copy(self.users.get(username, set())): + try: + await client.send(payload) + except: + pass + + async def send_to_chat(self, chat_id: str, payload: dict): + payload = ujson.dumps(payload) + for client in copy(self.chats.get(chat_id, set())): + try: + await client.send(payload) + except: + pass + + async def send_code(self, client, code: str, listener: str = None): + payload = {"cmd": "statuscode", "val": CODES[code]} + if listener: + payload["listener"] = listener + await client.send(ujson.dumps(payload)) + + async def log_peak_users(self): + if len(self.users.keys()) > self.peak_users.get("count", 0): + self.peak_users = { + "count": len(self.users.keys()), + "timestamp": uid.timestamp(jsonify=True) + } + await self.broadcast({ + "cmd": "direct", + "val": { + "mode": "peak", + "payload": self.peak_users + } + }) + + def subscribe_to_chat(self, client, chat_id: str): + if chat_id not in self.chats: + self.chats[chat_id] = set() + self.chats[chat_id].add(client) + client.chats.add(chat_id) + + def unsubscribe_from_chat(self, client, chat_id: str): + if client in self.chats.get(chat_id, set()): + self.chats[chat_id].remove(client) + if len(self.chats[chat_id]) == 0: + del self.chats[chat_id] + + async def kick_client(self, client, code: str = None): + if code: + await self.send_to_client(client, {"cmd": "direct", "val": CODES[code]}) + await client.close(code=1001, reason="") + + async def __handler__(self, client): + # Check whether repair mode is enabled + if redis.exists("repair_mode") == 1: + return await self.kick_client(client) + + # Get client's IP address + if config.ip_header and (config.ip_header in client.request_headers): + client.ip = client.request_headers[config.ip_header] + else: + if isinstance(client.remote_address, tuple): + client.ip = str(client.remote_address[0]) + else: + client.ip = client.remote_address + + # Check whether network is banned + network = networks.get_network(client.ip) + if network.banned: + return await self.kick_client(client, code="IPBanned") + + # Assign client properties and add to clients list + client.username = None + client.chats = set() + self.clients.add(client) + + # Subscribe client to livechat + self.subscribe_to_chat(client, "livechat") + + # Send current ulist to client + await self.send_to_client(client, {"cmd": "ulist", "val": self.ulist}) + + try: + # Handle incoming commands from the client + async for message in client: + # Check message size + if len(message) > 5000: + await self.send_code(client, "TooLarge") + continue + + try: + # Parse message + try: + message = ujson.loads(message) + except ujson.JSONDecodeError: + await self.send_code(client, "Datatype") + continue + + # Extract data + try: + cmd = message["cmd"] + val = message["val"] + listener = message.get("listener") + except KeyError: + await self.send_code(client, "Syntax") + continue + + # Convert direct command + if (cmd == "direct") and isinstance(val, dict) and ("cmd" in val) and ("val" in val): + cmd = val["cmd"] + val = val["val"] + + # Convert val for pmsg/pvar + if (cmd == "pmsg") or (cmd == "pvar"): + val = message + + # Check if command exists + if (cmd not in COMMANDS) or (not hasattr(self.command_handler, cmd)): + await self.send_code(client, "Invalid", listener=listener) + continue + + # Check if command is disabled + if cmd in DISABLED_COMMANDS: + await self.send_code(client, "Disabled", listener=listener) + continue + + # Run command + await getattr(self.command_handler, cmd)(client, val, listener) + + # Send OK statuscode + await self.send_code(client, "OK", listener) + except Exception as e: + if hasattr(e, "cl_code"): + await self.send_code(client, e.cl_code, listener=listener) + continue + else: + if config.development: + print(full_stack()) + await self.send_code(client, "Internal", listener=listener) + continue + except: + if config.development: + print(full_stack()) + finally: + # Remove client from clients list + self.clients.remove(client) + + # Remove client from users object + if client.username in self.users: + self.users[client.username].remove(client) + if len(self.users[client.username]) == 0: + del self.users[client.username] + await self.broadcast({"cmd": "ulist", "val": self.ulist}) + + # Unsubscribe client from all chats + for chat_id in client.chats: + self.unsubscribe_from_chat(client, chat_id) + + # Remove username from invisible users list + if (client.username in self.invisible_users) and (len(self.users.get(client.username, set())) == 0): + self.invisible_users.remove(client.username) + + async def main(self, host="localhost", port=3000): + async with websockets.serve(self.__handler__, host, port): + await asyncio.Future() + + +# Initialize the CL server +cl = server() diff --git a/src/common/database.py b/src/common/database.py new file mode 100644 index 0000000..e06e3f4 --- /dev/null +++ b/src/common/database.py @@ -0,0 +1,200 @@ +from pymongo import MongoClient, ASCENDING, DESCENDING, TEXT +from redis import Redis +import os +import secrets +import random +import time + +from src.common.util import config, full_stack, logging, migration + + +DB_COLLECTIONS = { + "config", + "users", + "netlog", + "posts", + "chats", + "reports", + "audit_log" +} +DB_VERSION = 2 + + +def create_collections(): + existing_collections = set(db.list_collection_names()) + for collection in DB_COLLECTIONS: + if collection not in existing_collections: + logging.info(f"Creating collection {collection}...") + db.create_collection(collection) + + +def delete_indexes(): + logging.info("Deleting indexes...") + for collection in db.list_collection_names(): + db[collection].drop_indexes() + + +def build_indexes(): + logging.info("Building indexes...") + + # Users + db.users.create_index([("lower_username", ASCENDING)], name="username", + unique=True) + db.users.create_index([("lower_username", TEXT), ("created", DESCENDING)], + name="search") + + # Networks + db.netlog.create_index([("users", ASCENDING)], name="users") + db.netlog.create_index([("last_used", ASCENDING)], + name="inactive_networks", + expireAfterSeconds=7776000, + partialFilterExpression={"banned": False, "range_banned": False}) + db.netlog.create_index([("range", ASCENDING)], name="ranges") + db.netlog.create_index([("range", ASCENDING), ("range_banned", ASCENDING)], + name="range_bans") + + # Posts + db.posts.create_index([("origin", ASCENDING), ("deleted_at", ASCENDING), + ("time", DESCENDING)], + name="latest_posts", + partialFilterExpression={"deleted_at": None}) + db.posts.create_index([("origin", ASCENDING), ("deleted_at", ASCENDING), + ("author", ASCENDING), ("time", DESCENDING)], + name="user_search", + partialFilterExpression={"deleted_at": None}) + db.posts.create_index([("origin", ASCENDING), ("deleted_at", ASCENDING), + ("content", TEXT), ("time", DESCENDING)], + name="content_search", + partialFilterExpression={"deleted_at": None}) + db.posts.create_index([("origin", ASCENDING), ("deleted_at", ASCENDING), + ("time", DESCENDING), ("author", ASCENDING)], + name="inbox", + partialFilterExpression={"origin": "inbox", "deleted_at": None}) + db.posts.create_index([("deleted_at", ASCENDING)], name="deleted_posts", + expireAfterSeconds=2592000, + partialFilterExpression={"deleted_at": {"$gt": 0}}) + + # Chats + db.chats.create_index([("members", ASCENDING), ("created", DESCENDING)], + name="user_chats") + db.chats.create_index([("invite_code", ASCENDING)], name="invite_code") + + # Reports + db.reports.create_index([("reputation", DESCENDING)], + name="report_reputation") + + # Audit log + db.audit_log.create_index([("time", DESCENDING)], name="logs", + expireAfterSeconds=2592000) + + +def create_config_items(): + logging.info("Creating config items...") + for item in [ + {"_id": "version", "database": DB_VERSION}, + {"_id": "security", "signing_key": secrets.token_bytes(2048)}, + {"_id": "filter", "whitelist": [], "blacklist": []} + ]: + try: + db.config.insert_one(item) + except: + pass + + +def setup_db(): + # Detect current database version + logging.info("Detecting database version...") + db_version = db.config.find_one({"_id": "version"}) + if db_version: + db_version = db_version.get("database") + elif len(db.list_collection_names()) > 0: + db_version = 1 + elif "Meower" in os.listdir(): + db_version = 0 + else: + create_collections() + delete_indexes() + build_indexes() + db_version = DB_VERSION + + # Make sure DB version exists + if db_version <= DB_VERSION: + logging.info(f"Database v{db_version} detected!") + else: + logging.error("Unknown databse version!") + logging.error("The database was probably created in a newer server build.") + logging.error("Exiting...") + exit() + + # Migrate database (if required) + if db_version < DB_VERSION: + logging.info("Migrating database...") + logging.warn("Please be patient and do not stop the server!") + if db_version == 0: + delete_indexes() + migration.migrate_from_v0(db) + build_indexes() + elif db_version == 1: + delete_indexes() + migration.migrate_from_v1(db) + build_indexes() + + # Set DB version + if db.config.count_documents({"_id": "version"}) > 0: + db.config.update_one({"_id": "version"}, {"$set": {"database": DB_VERSION}}) + else: + db.config.insert_one({"_id": "version", "database": DB_VERSION}) + + # Create default config items + create_config_items() + + +def count_pages(collection: str, query: dict) -> int: + total_items = db[collection].count_documents(query) + if total_items == 0: + pages = 0 + else: + if (total_items % 25) == 0: + if (total_items < 25): + pages = 1 + else: + pages = (total_items // 25) + else: + pages = (total_items // 25)+1 + + return pages + + +# Connect to MongoDB +try: + db = MongoClient(config.db_uri)[config.db_name] + db.command("ping") +except Exception as e: + logging.error(f"Failed connecting to database: {str(e)}") + exit() + + +# Connect to Redis +try: + redis = Redis( + host=config.redis_host, + port=config.redis_port, + db=config.redis_db, + password=config.redis_password + ) +except Exception as e: + logging.error(f"Failed connecting to Redis: {str(e)}") + exit() + + +# Setup database +logging.info("Acquiring lock to setup database...") +while redis.exists("db_setup_lock") == 1: + time.sleep(0.5) +try: + redis.set("db_setup_lock", "") + setup_db() +except: + print(full_stack()) +finally: + redis.delete("db_setup_lock") diff --git a/src/common/entities/audit_log.py b/src/common/entities/audit_log.py new file mode 100644 index 0000000..985fd29 --- /dev/null +++ b/src/common/entities/audit_log.py @@ -0,0 +1,57 @@ +import time + +from src.common.util import uid +from src.common.database import db, count_pages + + +class Log: + def __init__( + self, + _id: str, + action: str, + moderator: str, + data: dict, + time: int + ): + self.id = _id + self.action = action + self.moderator = moderator + self.data = data + self.time = time + + @property + def admin(self): + return { + "_id": self.id, + "action": self.action, + "moderator": self.moderator, + "data": self.data, + "time": self.time + } + + def delete(self): + db.audit_log.delete_one({"_id": self.id}) + + +def create_log(action: str, moderator: str, data: dict): + # Create log data + log_data = { + "_id": uid.uuid(), + "action": action, + "moderator": moderator, + "data": data, + "time": int(time.time()) + } + + # Insert log into database + db.audit_log.insert_one(log_data) + + # Return log object + return Log(**log_data) + + +def get_logs(page: int = 1) -> list[Log]: + return count_pages("audit_log", {}), [Log(**log) for log in db.audit_log.find({}, + sort=[("time", -1)], + skip=((page-1)*25), + limit=25)] diff --git a/src/common/entities/chats.py b/src/common/entities/chats.py new file mode 100644 index 0000000..657b1cc --- /dev/null +++ b/src/common/entities/chats.py @@ -0,0 +1,268 @@ +from secrets import token_urlsafe +from threading import Thread +import ujson +import time + +from src.common.entities import users, posts +from src.common.util import uid, errors, events +from src.common.database import db, redis, count_pages + + +LIVECHAT = { + "_id": "livechat", + "nickname": None, + "owner": None, + "members": [], + "invite_code": None, + "created": None +} + + +class Chat: + def __init__( + self, + _id: str, + nickname: str, + owner: str, + members: list, + invite_code: str, + created: int + ): + self.id = _id + self.nickname = nickname + self.owner = owner + self.members = members + self.invite_code = invite_code + self.created = created + + @property + def public(self): + return { + "_id": self.id, + "nickname": self.nickname, + "owner": self.owner, + "members": self.members, + "invite_code": self.invite_code, + "created": self.created + } + + def change_nickname(self, new_nickname: str) -> bool: + # Make sure the chat isn't livechat + if self.id == "livechat": + raise errors.MissingPermissions + + # Change nickname + self.nickname = new_nickname + db.chats.update_one({"_id": self.id}, {"$set": {"nickname": self.nickname}}) + + # Delete cache + redis.delete(f"chat:{self.id}") + + # Send chat update event + self.send_chat_update_event() + + def change_owner(self, username: str, actor: str = None) -> bool: + # Make sure the chat isn't livechat + if self.id == "livechat": + raise errors.MissingPermissions + + # Change owner + if username not in self.members: + raise errors.ChatMemberNotFound + self.owner = username + db.chats.update_one({"_id": self.id}, {"$set": {"owner": self.owner}}) + + # Delete cache + redis.delete(f"chat:{self.id}") + + # Send chat update event + self.send_chat_update_event() + + # Send message in chat + if actor: + posts.create_post(self.id, "Server", f"@{actor} transferred ownership of the group chat to @{username}.") + + def add_member(self, username: str, actor: str): + # Make sure the chat isn't livechat + if self.id == "livechat": + raise errors.MissingPermissions + + # Check if user is accepting chat invites + user = users.get_user(username) + if (username != actor) and (not user.accepting_invites): + raise errors.MissingPermissions + + # Add member + if username in self.members: + raise errors.AlreadyExists + self.members.append(username) + db.chats.update_one({"_id": self.id}, {"$addToSet": {"members": username}}) + + # Delete cache + redis.delete(f"chat:{self.id}") + + # Send chat update event + self.send_chat_update_event() + + # Send message in chat + if actor == username: + posts.create_post(self.id, "Server", f"@{username} joined the group chat via an invite code.") + else: + posts.create_post(self.id, "Server", f"@{actor} added @{username} to the group chat.") + posts.create_inbox_message(username, f"@{actor} added you to the group chat '{self.nickname}'.") + + def remove_member(self, username: str, actor: str): + # Make sure the chat isn't livechat + if self.id == "livechat": + raise errors.MissingPermissions + + # Remove member + if username not in self.members: + raise errors.NotFound + self.members.remove(username) + + if len(self.members) == 0: # Delete chat if no members are left + self.delete() + return + else: + if self.owner == username: # Transfer ownership if user was owner + self.owner = self.members[0] + db.chats.update_one({"_id": self.id}, {"$set": {"owner": self.owner}, "$pull": {"members": username}}) + + # Delete cache + redis.delete(f"chat:{self.id}") + + # Send chat update event + self.send_chat_update_event() + + # Send message in chat + if actor == username: + posts.create_post(self.id, "Server", f"@{username} left the group chat.") + else: + posts.create_post(self.id, "Server", f"@{actor} removed @{username} from the group chat.") + posts.create_inbox_message(username, f"@{actor} removed you from the group chat '{self.nickname}'.") + + def set_chat_state(self, username: str, state: int): + events.send_event("update_chat_state", { + "chatid": self.id, + "u": username, + "state": state + }) + + def reset_invite_code(self): + # Make sure the chat isn't livechat + if self.id == "livechat": + raise errors.MissingPermissions + + # Update chat invite code + self.invite_code = token_urlsafe(5) + db.chats.update_one({"_id": self.id}, {"$set": {"invite_code": self.invite_code}}) + + # Send chat update event + self.send_chat_update_event() + + def send_chat_update_event(self): + events.send_event("update_chat", self.public) + + def delete(self): + # Make sure the chat isn't livechat + if self.id == "livechat": + raise errors.MissingPermissions + + # Delete properties + self.nickname = None + self.owner = None + self.members = [] + self.invite_code = None + self.created = None + + # Delete chat from database + db.chats.delete_one({"_id": self.id}) + + # Delete cache + redis.delete(f"chat:{self.id}") + + # Send chat update event + self.send_chat_update_event() + + # Schedule chat messages for deletion + Thread(target=db.posts.update_many, args=({"origin": self.id, "deleted_at": None}, {"$set": {"delete_after": int(time.time())}},)).start() + + +def create_chat(nickname: str, owner: str) -> Chat: + # Create chat ID + chat_id = uid.uuid() + + # Create chat data + chat_data = { + "_id": chat_id, + "nickname": nickname, + "owner": owner, + "members": [owner], + "invite_code": token_urlsafe(5), + "created": int(time.time()) + } + + # Insert chat into database + db.chats.insert_one(chat_data) + + # Add chat to cache + redis.set(f"chat:{chat_id}", ujson.dumps(chat_data), ex=120) + + # Get chat object + chat = Chat(**chat_data) + + # Send chat update event + chat.send_chat_update_event() + + # Return chat object + return chat + + +def get_chat(chat_id: str) -> Chat: + # Get livechat + if chat_id == "livechat": + return Chat(**LIVECHAT) + + # Get chat from cache + chat_data = redis.get(f"chat:{chat_id}") + if chat_data: + chat_data = ujson.loads(chat_data) + + # Get chat from database and add to cache + if not chat_data: + chat_data = db.chats.find_one({"_id": chat_id}) + if chat_data: + redis.set(f"chat:{chat_id}", ujson.dumps(chat_data), ex=120) + + # Return chat object + if chat_data: + return Chat(**chat_data) + else: + raise errors.NotFound + + +def get_chat_by_invite_code(invite_code: str) -> Chat: + # Get chat from database and add to cache + chat_data = db.chats.find_one({"invite_code": invite_code}) + if chat_data: + chat_id = chat_data["_id"] + redis.set(f"chat:{chat_id}", ujson.dumps(chat_data), ex=120) + + # Return chat object + if chat_data: + return Chat(**chat_data) + else: + raise errors.NotFound + + +def get_users_chats(username: str, page: int = 1) -> list[Chat]: + query = {"members": {"$all": [username]}} + return count_pages("chats", query), [Chat(**chat) for chat in db.chats.find(query, + sort=[("created", -1)], + skip=(((page-1)*25) if page else 0), + limit=(25 if page else 0))] + + +def get_all_chat_ids(username: str) -> list[str]: + return [chat["_id"] for chat in db.chats.find({"members": {"$all": [username]}}, projection={"_id": 1})] diff --git a/src/common/entities/networks.py b/src/common/entities/networks.py new file mode 100644 index 0000000..d3abbea --- /dev/null +++ b/src/common/entities/networks.py @@ -0,0 +1,128 @@ +import ipaddress +import requests +import time + +from src.common.util import config, errors, events +from src.common.database import db + + +class Network: + def __init__( + self, + _id: str, + users: list = [], + last_user: str = None, + proxy: bool = None, + country: str = None, + banned: bool = False, + last_used: int = None + ): + self.ip = _id + self.users = users + self.last_user = last_user + self.proxy = proxy + self.country = country + self.banned = banned + self.last_used = last_used + + @property + def admin(self): + return { + "_id": self.ip, + "ip": self.ip, + "users": self.users, + "last_user": self.last_user, + "proxy": self.proxy, + "country": self.country, + "banned": self.banned, + "last_used": (self.last_used if self.last_used else None) + } + + def log_user(self, username: str): + # Update network + if username not in self.users: + self.users.append(username) + self.last_user = username + self.last_used = int(time.time()) + db.netlog.update_one({"_id": self.ip}, { + "$addToSet": {"users": username}, + "$set": {"last_user": self.last_user, "last_used": self.last_used} + }) + + # Update user + db.users.update_one({"_id": username}, {"$set": {"last_ip": self.ip}}) + + def set_ban_state(self, banned: bool): + # Set ban status + self.banned = banned + db.netlog.update_one({"_id": self.ip}, {"$set": {"banned": self.banned}}) + + # Kick users if network was banned + if self.banned: + events.send_event("kick_network", { + "ip": self.ip, + "code": "IPBanned" + }) + + def delete(self): + db.netlog.delete_one({"_id": self.ip}) + + +def get_iphub_data(ip_address: str) -> dict: + if config.iphub_key: + iphub_resp = requests.get(f"https://v2.api.iphub.info/ip/{ip_address}", + headers={"X-Key": config.iphub_key}) + if iphub_resp.status_code == 200: + iphub_data = iphub_resp.json() + else: + iphub_data = {} + else: + iphub_data = {} + + return iphub_data + + +def get_network(ip_address: str) -> Network: + # Get IP range + ip_obj = ipaddress.ip_address(ip_address) + if ip_obj.version == 4: + ip_range = ipaddress.IPv4Network(ip_obj.exploded + "/24") + elif ip_obj.version == 6: + ip_range = ipaddress.IPv4Network(ip_obj.exploded + "/32") + else: + raise errors.IllegalIP + + # Get network from database + network = db.netlog.find_one({"_id": ip_address}) + + # Create network if it doesn't exist or update network if it doesn't have IPHub data + if not network: + iphub_data = get_iphub_data(ip_address) + network = { + "_id": ip_address, + "range": ip_range, + "users": [], + "last_user": None, + "proxy": (iphub_data.get("block") == 1), + "country": iphub_data.get("countryName"), + "banned": False, + "range_banned": False + } + db.netlog.insert_one(network) + elif "country" not in network: + iphub_data = get_iphub_data(ip_address) + if ("block" in iphub_data) and ("countryName" in iphub_data): + network["proxy"] = (iphub_data.get("block") == 1) + network["country"] = iphub_data.get("countryName") + db.netlog.update_one({"_id": ip_address}, {"$set": { + "proxy": network["proxy"], + "country": network["country"] + }}) + + # Sync IP range ban + if (not network.get("range_banned")) and (db.netlog.count_documents({"range": ip_range, "range_banned": True}) > 0): + network["range_banned"] = True + db.netlog.update_one({"_id": ip_address}, {"$set": {"range_banned": True}}) + + # Return network object + return Network(**network) diff --git a/src/common/entities/posts.py b/src/common/entities/posts.py new file mode 100644 index 0000000..882e3a9 --- /dev/null +++ b/src/common/entities/posts.py @@ -0,0 +1,247 @@ +import time + +from src.common.entities import users, chats, reports, audit_log +from src.common.util import uid, errors, events, profanity +from src.common.database import db, count_pages + + +class Post: + def __init__( + self, + _id: str, + type: int, + origin: str, + author: str, + time: int, + content: str, + unfiltered_content: str = None, + deleted_at: int = None, + mod_deleted: bool = False + ): + self.id = _id + self.type = type + self.origin = origin + self.author = author + self.time = time + self.content = content + self.unfiltered_content = unfiltered_content + self.deleted_at = deleted_at + self.mod_deleted = mod_deleted + + @property + def public(self): + return { + "_id": self.id, + "post_id": self.id, + "type": self.type, + "post_origin": self.origin, + "u": self.author, + "t": uid.timestamp(epoch=self.time, jsonify=True), + "p": self.content, + "unfiltered_p": self.unfiltered_content, + "isDeleted": (self.deleted_at is not None) + } + + def has_access(self, user): + # Default permissions if a user isn't specified + if not user: + return ((self.origin == "home") and (not self.deleted_at)) + + # Get user + if isinstance(user, str): + user = users.get_user(user) + + # Check user level + if user.lvl >= 1: + return True + + # Check if post is deleted + if self.deleted_at: + return False + + # Check if user has access to chat + if self.origin == "home": + return True + else: + chat = chats.get_chat(self.origin) + return (user.username in chat.members) + + def edit(self, new_content: str): + # Set properties + self.content = profanity.censor(new_content) + self.unfiltered_content = (None if (self.content == new_content) else new_content) + db.posts.update_one({"_id": self.id}, {"$set": { + "content": self.content, + "unfiltered_content": self.unfiltered_content + }}) + + # Send post update event + events.send_event("update_post", self.public) + + def delete(self, actor: str): + # Set properties + self.deleted_at = int(time.time()) + self.mod_deleted = (actor != self.author) + db.posts.update_one({"_id": self.id}, {"$set": { + "deleted_at": self.deleted_at, + "mod_deleted": self.mod_deleted + }}) + + # Send post update event + events.send_event("update_post", self.public) + + if self.mod_deleted: + # Close report + try: + report = reports.get_report(self.id) + except errors.NotFound: + pass + else: + report.close(True) + + # Create audit log item + audit_log.create_log("delete_post", actor, {"id": self.id}) + + def restore(self): + # Set properties + self.deleted_at = None + self.mod_deleted = False + db.posts.update_one({"_id": self.id}, {"$unset": { + "deleted_at": "", + "mod_deleted": "" + }}) + + # Send post update event + events.send_event("update_post", self.public) + + +def create_post(origin: str, author: str, content: str) -> Post: + # Create post data + post_data = { + "_id": uid.uuid(), + "type": 1, + "origin": origin, + "author": author, + "time": int(time.time()), + "content": content + } + + # Filter content + post_data["content"] = profanity.censor(content) + if post_data["content"] != content: + post_data["unfiltered_content"] = content + + # Insert post into database + if origin != "livechat": + db.posts.insert_one(post_data) + + # Get post object + post = Post(**post_data) + + # Send post creation event + events.send_event("create_post", post.public) + + # Return post object + return post + + +def create_inbox_message(username: str, content: str) -> Post: + # Create post data + post_data = { + "_id": uid.uuid(), + "type": 2, + "origin": "inbox", + "author": username, + "time": int(time.time()), + "content": content + } + + # Insert post into database + db.posts.insert_one(post_data) + + # Update user + db.users.update_many({"_id": username}, {"$set": {"unread_inbox": True}}) + + # Get post object + post = Post(**post_data) + + # Send post creation event + events.send_event("create_post", post.public) + + # Return post object + return post + + +def create_announcement(content: str) -> Post: + # Create post data + post_data = { + "_id": uid.uuid(), + "type": 2, + "origin": "inbox", + "author": "Server", + "time": int(time.time()), + "content": content + } + + # Insert post into database + db.posts.insert_one(post_data) + + # Update users + db.users.update_many({}, {"$set": {"unread_inbox": True}}) + + # Get post object + post = Post(**post_data) + + # Send post creation event + events.send_event("create_post", post.public) + + # Return post object + return post + + +def get_post(post_id: str) -> Post: + # Get post from database + post = db.posts.find_one({"_id": post_id}) + + # Return post object + if post: + return Post(**post) + else: + raise errors.NotFound + + +def get_posts(origin: str, author: str = None, page: int = 1) -> list[Post]: + query = { + "origin": origin, + "deleted_at": None + } + if author: + query["author"] = author + return count_pages("posts", query), [Post(**post) for post in db.posts.find(query, + sort=[("time", -1)], + skip=((page-1)*25), + limit=25)] + + +def search_posts(origin: str, content_query: str = None, page: int = 1) -> list[Post]: + query = { + "origin": origin, + "deleted_at": None, + "$text": {"$search": content_query} + } + return count_pages("posts", query), [Post(**post) for post in db.posts.find(query, + sort=[("time", -1)], + skip=((page-1)*25), + limit=25)] + + +def get_inbox_messages(username: str, page: int = 1) -> list[Post]: + query = { + "origin": "inbox", + "deleted_at": None, + "author": {"$in": [username, "Server"]} + } + return count_pages("posts", query), [Post(**post) for post in db.posts.find(query, + sort=[("time", -1)], + skip=((page-1)*25), + limit=25)] diff --git a/src/common/entities/reports.py b/src/common/entities/reports.py new file mode 100644 index 0000000..75c0d1b --- /dev/null +++ b/src/common/entities/reports.py @@ -0,0 +1,133 @@ +import time + +from src.common.entities import posts, users +from src.common.util import errors +from src.common.database import db, count_pages + + +class Report: + def __init__( + self, + _id: str, + type: int, + reports: list, + score: int, + created: int + ): + self.id = _id + self.type = type + self.reports = reports + self.score = score + self.created = created + + @property + def admin(self): + data = self.content.public + data.update({ + "type": self.type, + "reports": self.reports, + "score": self.score + }) + return data + + @property + def content(self): + if self.type == 0: + return posts.get_post(self.id) + elif self.type == 1: + return users.get_user(self.id) + else: + raise errors.NotFound + + def add_report(self, username: str, user_reputation: float): + if username not in self.reports: + self.reports.append(username) + self.score += user_reputation + db.reports.update_one({"_id": self.id}, {"$set": { + "reports": self.reports, + "score": self.score + }}) + + def close(self, status: bool|None, actor: str = None): + if status is not None: + # Construct inbox message + if status: + content = "We took action on one of your recent reports against @" + else: + content = "We could not take action on one of your recent reports against @" + if self.type == 0: + content += f"{self.content.author}'s post." + elif self.type == 1: + content += f"{self.content.username}." + if status: + content += " Thank you for your help with keeping Meower a safe and welcoming place!" + else: + content += " The content you reported was not severe enough to warrant action being taken. We still want to thank you for your help with keeping Meower a safe and welcoming place!" + + # Send inbox messages + for username in self.reports: + posts.create_inbox_message(username, content) + + # Update report reputation + db.users.update_many({"_id": {"$in": self.reports}}, {"$inc": { + "report_reputation": (0.01 if status else -0.01) + }}) + + # Delete report + db.reports.delete_one({"_id": self.id}) + + +def create_report(content_type: int, content_id: str, username: str, user_reputation: float): + try: + report = get_report(content_id) + except errors.NotFound: + # Make sure content exists + if content_type == 0: + posts.get_post(content_id) + elif content_type == 1: + users.get_user(content_id) + + # Create report data + report_data = { + "_id": content_id, + "type": content_type, + "reports": [username], + "score": user_reputation, + "created": int(time.time()) + } + + # Insert report into database + db.reports.insert_one(report_data) + + # Return report object + return Report(**report_data) + else: + report.add_report(username, user_reputation) + return report + + +def close_report(content_id: str, status: bool|None, actor: str = None): + try: + report = get_report(content_id) + except errors.NotFound: + pass + else: + report.close(status, actor) + + +def get_report(report_id: str): + # Get report from database + report_data = db.reports.find_one({"_id": report_id}) + + # Return report object + if report_data: + return Report(**report_data) + else: + raise errors.NotFound + + +def get_reports(page: int = 1) -> list[Report]: + return count_pages("reports", {}), [Report(**report) for report in db.reports.find({}, + sort=[("score", -1)], + skip=((page-1)*25), + limit=25)] diff --git a/src/common/entities/users.py b/src/common/entities/users.py new file mode 100644 index 0000000..c3c990d --- /dev/null +++ b/src/common/entities/users.py @@ -0,0 +1,526 @@ +from copy import copy +from base64 import b64encode, b64decode +import bcrypt +import time +import ujson +import re + +from src.common.entities import posts, chats, reports, audit_log +from src.common.util import uid, regex, errors, security, events +from src.common.database import db, redis, count_pages + + +CONFIG_KEYS = { + "invisible": bool, + "unread_inbox": bool, + "accepting_invites": bool, + "theme": str, + "mode": bool, + "layout": str, + "debug": bool, + "sfx": bool, + "bgm": bool, + "bgm_song": int, + "pfp_data": int, + "quote": str +} + + +# Default users +SERVER = { + "_id": "Server", + "lower_username": "server", + "uuid": "0", + "created": 0 +} +DELETED = { + "_id": "Deleted", + "lower_username": "deleted", + "uuid": "1", + "created": 0 +} +MEOWER = { + "_id": "Meower", + "lower_username": "meower", + "uuid": "2", + "created": 0 +} + + +class User: + def __init__( + self, + _id: str, + lower_username: str, + uuid: str, + created: int, + delete_after: int = None, + email: str = None, + pswd: str = None, + token_version: int = 0, + lvl: int = 0, + report_reputation: float = 0, + last_ip: str = None, + banned_until: int = None, + invisible: bool = False, + unread_inbox: bool = False, + accepting_invites: bool = True, + theme: str = "orange", + mode: bool = True, + layout: str = "new", + debug: bool = False, + sfx: bool = True, + bgm: bool = True, + bgm_song: int = 2, + pfp_data: int = 1, + quote: str = "" + ): + self.username = _id + self.lower_username = lower_username + self.uuid = uuid + self.created = created + self.delete_after = delete_after + self.email = email + self.pswd = pswd + self.token_version = token_version + self.lvl = lvl + self.report_reputation = report_reputation + self.last_ip = last_ip + self.banned_until = banned_until + self.invisible= invisible + self.unread_inbox = unread_inbox + self.accepting_invites = accepting_invites + self.theme = theme + self.mode = mode + self.layout = layout + self.debug = debug + self.sfx = sfx + self.bgm = bgm + self.bgm_song = bgm_song + self.pfp_data = pfp_data + self.quote = quote + + @property + def public(self): + return { + "_id": self.username, + "lower_username": self.lower_username, + "uuid": self.uuid, + "created": self.created, + "lvl": self.lvl, + "banned": self.banned, + "pfp_data": self.pfp_data, + "quote": self.quote + } + + @property + def client(self): + return { + "_id": self.username, + "lower_username": self.lower_username, + "uuid": self.uuid, + "created": self.created, + "delete_after": (self.delete_after if self.delete_after else None), + "email": self.email, + "lvl": self.lvl, + "report_reputation": self.report_reputation, + "banned": self.banned, + "invisible": self.invisible, + "unread_inbox": self.unread_inbox, + "accepting_invites": self.accepting_invites, + "theme": self.theme, + "mode": self.mode, + "layout": self.layout, + "debug": self.debug, + "sfx": self.sfx, + "bgm": self.bgm, + "bgm_song": self.bgm_song, + "pfp_data": self.pfp_data, + "quote": self.quote + } + + @property + def banned(self): + if not isinstance(self.banned_until, int): + return False + elif (self.banned_until == -1) or (self.banned_until > time.time()): + return True + else: + return False + + def update_config(self, new_config: dict): + # Update config keys + for key, value in copy(new_config).items(): + if key not in CONFIG_KEYS: + del new_config[key] + elif not isinstance(value, CONFIG_KEYS[key]): + del new_config[key] + elif isinstance(value, str) and (len(value) > 360): + del new_config[key] + elif isinstance(value, int) and (value > 255): + del new_config[key] + else: + setattr(self, key, value) + db.users.update_one({"_id": self.username}, {"$set": new_config}) + + # Delete cache + redis.delete(f"user:{self.username}") + + # Send config update event + events.send_event("update_config", self.client) + + def change_username(self, new_username: str) -> bool: + # Check if new username is available + if not username_exists(new_username, case_sensitive=False): + raise errors.AlreadyExists + + # Get old username + old_username = copy(self.username) + + # Create new user + db.users.insert_one({ + "_id": new_username, + "lower_username": new_username.lower(), + "uuid": self.uuid, + "created": self.created, + "delete_after": self.delete_after, + "email": self.email, + "pswd": self.pswd, + "token_key": self.token_key, + "lvl": self.lvl, + "report_reputation": self.report_reputation, + "last_ip": self.last_ip, + "banned_until": self.banned_until, + "invisible": self.invisible, + "unread_inbox": self.unread_inbox, + "accepting_invites": self.accepting_invites, + "theme": self.theme, + "mode": self.mode, + "layout": self.layout, + "debug": self.debug, + "sfx": self.sfx, + "bgm": self.bgm, + "bgm_song": self.bgm_song, + "pfp_data": self.pfp_data, + "quote": self.quote + }) + + # Update attributes + self.username = new_username + self.lower_username = new_username.lower() + + # Update all networks + db.netlog.update_many({"users": old_username}, {"$set": {"users.$": self.username}}) + + # Update all posts + db.posts.update_many({"origin": old_username}, {"$set": {"origin": self.username}}) + + # Update all chats + db.chats.update_many({"members": old_username}, {"$set": {"members.$": self.username}}) + db.chats.update_many({"members": old_username, "owner": old_username}, {"$set": {"owner": self.username}}) + + # Update report + report = db.reports.find_one({"_id": old_username}) + if report: + report["_id"] = self.username + db.reports.insert_one(report) + db.reports.delete_one({"_id": old_username}) + + # Delete old user + db.users.delete_one({"_id": old_username}) + + # Delete cache + redis.delete(f"user:{old_username}") + redis.delete(f"user:{self.username}") + + # Kick old user + events.send_event("kick_user", {"username": old_username}) + + def check_password(self, password: str) -> bool: + # Check if user is waiting to be deleted + if self.delete_after and (self.delete_after <= time.time()): + return False + + return bcrypt.checkpw(password.encode(), self.pswd.encode()) + + def change_password(self, new_password: str): + # Set new password + self.pswd = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt(12)).decode() + db.users.update_one({"_id": self.username}, {"$set": {"pswd": self.pswd}}) + + # Delete cache + redis.delete(f"user:{self.username}") + + def generate_token(self) -> str: + # Generate data + data = b64encode(f"{self.uuid}:{self.token_version}:{str(time.time())}".encode()) + + # Generate new signature + signature = security.sign_data(data) + + # Return token + return f"{data.decode()}.{signature.decode()}" + + def validate_token(self, token: str) -> bool: + # Decode token + try: + data, signature = token.split(".") + except: + return False + + # Check data properties + try: + user_uuid, version, timestamp = b64decode(data.encode()).decode().split(":") + version = int(version) + timestamp = float(timestamp) + except: + return False + else: + if (user_uuid != self.uuid) or (version != self.token_version): + return False + + # Check token signature + return security.validate_signature(signature.encode(), data.encode()) + + def revoke_sessions(self): + # Increment token version + self.token_version += 1 + db.users.update_one({"_id": self.username}, {"$set": {"token_version": self.token_version}}) + + # Delete cache + redis.delete(f"user:{self.username}") + + # Kick user + events.send_event("kick_user", {"username": self.username}) + + def set_level(self, level: int): + # Set user level + self.lvl = level + db.users.update_one({"_id": self.username}, {"$set": {"lvl": self.lvl}}) + + # Delete cache + redis.delete(f"user:{self.username}") + + # Send config update event + events.send_event("update_config", self.client) + + def clear_posts(self, moderator: str = None): + # Delete posts + for post in db.posts.find({"origin": {"$ne": "inbox"}, "deleted_at": None, "author": self.username}): + posts.Post(**post).delete(moderator) + + # Create audit log item + if moderator: + audit_log.create_log("clear_user_posts", moderator, { + "username": self.username + }) + + def alert(self, content: str, moderator: str = None): + # Create inbox message + posts.create_inbox_message(self.username, f"Message from a moderator: {content}") + + # Close report + reports.close_report(self.username, True, moderator) + + # Create audit log item + if moderator: + audit_log.create_log("alert_user", moderator, { + "username": self.username, + "content": content + }) + + def kick(self, moderator: str = None): + # Kick user + events.send_event("kick_user", { + "username": self.username, + "code": "Kicked" + }) + + # Close report + reports.close_report(self.username, True, moderator) + + # Create audit log item + if moderator: + audit_log.create_log("kick_user", moderator, {"username": self.username}) + + def ban(self, expires: int = -1, moderator: str = None): + # Set ban status + self.banned_until = expires + db.users.update_one({"_id": self.username}, {"$set": {"banned_until": self.banned_until}}) + + # Delete cache + redis.delete(f"user:{self.username}") + + # Send config update event + events.send_event("update_config", self.client) + + # Close report + reports.close_report(self.username, True, moderator) + + # Create audit log item + if moderator: + audit_log.create_log("ban_user", moderator, { + "username": self.username, + "expires": expires + }) + + def unban(self, moderator: str = None): + # Set ban status + self.banned_until = None + db.users.update_one({"_id": self.username}, {"$set": {"banned_until": self.banned_until}}) + + # Delete cache + redis.delete(f"user:{self.username}") + + # Send config update event + events.send_event("update_config", self.client) + + # Create audit log item + if moderator: + audit_log.create_log("pardon_user", moderator, { + "username": self.username + }) + + def terminate(self, moderator: str = None): + # Ban user + self.ban(moderator=moderator) + + # Clear posts + self.clear_posts(moderator=moderator) + + # Schedule account for deletion (14 days) + self.schedule_deletion(delay=2592000) + + # Create audit log item + if moderator: + audit_log.create_log("terminate_user", moderator, { + "username": self.username + }) + + def schedule_deletion(self, delay: int = 259200): + # Set deletion delay (72 hours default) + self.delete_after = int(time.time()+delay) + db.users.update_one({"_id": self.username}, {"$set": {"delete_after": self.delete_after}}) + + # Revoke sessions + self.revoke_sessions() + + # Delete cache + redis.delete(f"user:{self.username}") + + # Kick user + events.send_event("kick_user", {"username": self.username}) + + def cancel_scheduled_deletion(self, send_inbox_message: bool = True): + # Reset deletion timestamp + self.delete_after = None + db.users.update_one({"_id": self.username}, {"$unset": {"delete_after": ""}}) + + # Delete cache + redis.delete(f"user:{self.username}") + + # Send inbox message telling the user the deletion was cancelled + if send_inbox_message: + posts.create_inbox_message(self.username, f"Your account was scheduled for deletion but you logged back in. Your account is no longer scheduled for deletion!") + + def delete(self): + # Update all networks + db.netlog.update_many({"users": self.username}, {"$pull": {"users": self.username}}) + + # Delete all posts + db.posts.delete_many({"origin": {"$exists": True}, "deleted_at": None, "author": self.username}) + + # Update all chats + _, user_chats = chats.get_users_chats(self.username, page=None) + for chat in user_chats: + chat.remove_member(self.username, "Server") + + # Close report + reports.close_report(self.username, None) + + # Delete user + db.users.delete_one({"_id": self.username}) + + # Delete cache + redis.delete(f"user:{self.username}") + + # Kick user + events.send_event("kick_user", {"username": self.username}) + + +def username_exists(username: str, case_sensitive: bool = True) -> bool: + if (case_sensitive and (username in {"Server", "Deleted", "Meower"})) or ((not case_sensitive) and (username.lower() in {"server", "deleted", "meower"})): + return True + elif redis.exists(f"user:{username}") == 1: + return True + elif case_sensitive: + return (db.users.count_documents({"_id": username}) > 0) + else: + return (db.users.count_documents({"lower_username": username.lower()}) > 0) + + +def create_user(username: str, password: str) -> User: + # Check if username is valid + if not re.fullmatch(regex.USERNAME_VALIDATION, username): + raise errors.IllegalCharacters + + # Check if username is available + if username_exists(username, case_sensitive=False): + raise errors.AlreadyExists + + # Create user data + user_data = { + "_id": username, + "lower_username": username.lower(), + "uuid": uid.uuid(), + "created": int(time.time()), + "pswd": bcrypt.hashpw(password.encode(), bcrypt.gensalt(12)).decode() + } + + # Insert user into database + db.users.insert_one(user_data) + + # Add user to cache + redis.set(f"user:{username}", ujson.dumps(user_data), ex=120) + + # Send welcome inbox message + posts.create_inbox_message(username, "Welcome to Meower! We welcome you with open arms! You can get started by making friends in the global chat or home, or by searching for people and adding them to a group chat. We hope you have fun!") + + # Return user object + return User(**user_data) + + +def get_user(username: str) -> User: + if username == "Server": + return User(**SERVER) + elif username == "Deleted": + return User(**DELETED) + elif username == "Meower": + return User(**MEOWER) + else: + # Get user from cache + user_data = redis.get(f"user:{username}") + if user_data: + user_data = ujson.loads(user_data) + + # Get user from database and add to cache + if not user_data: + user_data = db.users.find_one({"_id": username}) + if user_data: + redis.set(f"user:{username}", ujson.dumps(user_data), ex=120) + + # Return user object + if user_data: + return User(**user_data) + else: + raise errors.NotFound + + +def search_users(username_query: str, page: int = 1) -> list[User]: + query = { + "$text": {"$search": username_query.lower()} + } + return count_pages("users", query), [User(**user) for user in db.users.find(query, + sort=[("created", -1)], + skip=((page-1)*25), + limit=25)] diff --git a/src/common/util/__init__.py b/src/common/util/__init__.py new file mode 100644 index 0000000..b5c8d73 --- /dev/null +++ b/src/common/util/__init__.py @@ -0,0 +1,84 @@ +from dotenv import load_dotenv +import os +import traceback +import sys + +from src.common.util import errors + + +class Config: + def __init__(self): + # Load environment variables + load_dotenv() + + # Set properties + for key, datatype, default in [ + ("DEVELOPMENT", bool, False), + ("IP_HEADER", str, None), + ("IPHUB_KEY", str, None), + ("BLOCK_PROXIES", bool, False), + ("HOST", str, "0.0.0.0"), + ("API_PORT", int, 3000), + ("CL3_PORT", int, 3001), + ("DB_URI", str, "mongodb://127.0.0.1:27017"), + ("DB_NAME", str, "meowerserver"), + ("REDIS_HOST", str, "127.0.0.1"), + ("REDIS_PORT", int, 6379), + ("REDIS_DB", int, 0), + ("REDIS_PASSWORD", str, None) + ]: + env_vars = {} + if key in os.environ: + if datatype is bool: + env_vars[key] = (os.environ[key] == "true") + else: + env_vars[key] = datatype(os.environ[key]) + setattr(self, key.lower(), env_vars.get(key.upper(), default)) + + +def display_startup(): + print(f"Meower -- {version} ({build_time})") + + +def full_stack(): + exc = sys.exc_info()[0] + if exc is not None: + f = sys.exc_info()[-1].tb_frame.f_back + stack = traceback.extract_stack(f) + else: + stack = traceback.extract_stack()[:-1] + trc = 'Traceback (most recent call last):\n' + stackstr = trc + ''.join(traceback.format_list(stack)) + if exc is not None: + stackstr += ' ' + traceback.format_exc().lstrip(trc) + return stackstr + + +def validate(data: dict, expected: dict[str, tuple[type, int, int]], optional: list[str] = []): + if (not isinstance(data, dict)) and (set(expected.keys()) != set(optional)): + raise errors.InvalidDatatype + + for key, (datatype, min, max) in expected.items(): + if key in data: + if datatype and (not isinstance(data[key], datatype)): + raise errors.InvalidDatatype + + if datatype == str: + if min and (len(data[key]) < min): + raise errors.TooShort + + if max and (len(data[key]) > max): + raise errors.TooLarge + elif datatype == int: + if min and (data[key] < min): + raise errors.InvalidSyntax + + if max and (data[key] > max): + raise errors.InvalidSyntax + elif key not in optional: + raise errors.InvalidSyntax + + +config = Config() +version = "0.5.0-beta" +build_time = 1680654547 diff --git a/src/common/util/errors.py b/src/common/util/errors.py new file mode 100644 index 0000000..bc83e82 --- /dev/null +++ b/src/common/util/errors.py @@ -0,0 +1,44 @@ +class InvalidSyntax(Exception): + cl_code = "Syntax" + +class InvalidDatatype(Exception): + cl_code = "Datatype" + +class IllegalCharacters(Exception): + cl_code = "IllegalChars" + +class TooShort(Exception): + cl_code = "Syntax" + +class TooLarge(Exception): + cl_code = "TooLarge" + +class NotFound(Exception): + cl_code = "IDNotFound" + +class AlreadyExists(Exception): + cl_code = "IDExists" + +class NotAuthenticated(Exception): + cl_code = "IDRequired" + +class MissingPermissions(Exception): + cl_code = "MissingPermissions" + +class UsernameInvalid(Exception): + cl_code = "IDNotFound" + +class InvalidPassword(Exception): + cl_code = "InvalidPassword" + +class Ratelimited(Exception): + cl_code = "Ratelimited" + +class UserBanned(Exception): + cl_code = "Banned" + +class IPBanned(Exception): + cl_code = "IPBanned" + +class IllegalIP(Exception): + cl_code = "IllegalIP" diff --git a/src/common/util/events.py b/src/common/util/events.py new file mode 100644 index 0000000..48e8423 --- /dev/null +++ b/src/common/util/events.py @@ -0,0 +1,25 @@ +from threading import Thread +import ujson +import asyncio + +from src.common.database import redis + + +def send_event(event: str, payload: dict): + redis.publish("meower", ujson.dumps({"event": event, "payload": payload})) + + +def add_event_listener(callback: callable): + def run(): + pubsub = redis.pubsub() + pubsub.subscribe("meower") + for message in pubsub.listen(): + try: + message = ujson.loads(message["data"]) + asyncio.run(callback(message["event"], message["payload"])) + except: + continue + + runner_thread = Thread(target=run) + runner_thread.daemon = True + runner_thread.start() diff --git a/src/common/util/logging.py b/src/common/util/logging.py new file mode 100644 index 0000000..b861aea --- /dev/null +++ b/src/common/util/logging.py @@ -0,0 +1,23 @@ +from datetime import datetime + +class PrintColors: + """ + Nice colors used for when printing logs to the console. + """ + + GREEN = "\033[92m" + YELLOW = "\033[93m" + RED = "\033[91m" + END = "\033[0m" + +def success(event: str): + print("{0}[{1}]".format(PrintColors.GREEN, datetime.now().strftime("%m/%d/%Y %H:%M.%S")), "[SUCCESS]", event, PrintColors.END) + +def info(event: str): + print("[{0}]".format(datetime.now().strftime("%m/%d/%Y %H:%M.%S")), "[INFO]", event, "") + +def warn(event: str): + print("{0}[{1}]".format(PrintColors.YELLOW, datetime.now().strftime("%m/%d/%Y %H:%M.%S")), "[WARNING]", event, PrintColors.END) + +def error(event: str): + print("{0}[{1}]".format(PrintColors.RED, datetime.now().strftime("%m/%d/%Y %H:%M.%S")), "[ERROR]", event, PrintColors.END) diff --git a/src/common/util/migration.py b/src/common/util/migration.py new file mode 100644 index 0000000..6ee0813 --- /dev/null +++ b/src/common/util/migration.py @@ -0,0 +1,326 @@ +from datetime import datetime +from copy import copy +import os +import ujson +import time +import secrets + +from src.common.util import uid, logging, events + + +def migrate_from_v0(db): + # Migrate users + try: + logging.info("Migrating users...") + usernames = set() + lower_usernames = set() + for username in os.listdir("./Meower/Userdata"): + try: + f = open(f"./Meower/Userdata/{username}", "r") + user = ujson.load(f) + f.close() + if username.lower() in lower_usernames: + raise Exception("Duplicate username") + db.users.insert_one({ + "_id": str(username), + "lower_username": str(username.lower()), + "uuid": str(user.get("uuid", uid.uuid())), + "created": int(user.get("created", int(time.time()))), + "pswd": str(user.get("pswd")), + "lvl": int(user.get("lvl", 0)), + "banned_until": (-1 if user.get("banned") else None), + "theme": str(user.get("theme", "orange")), + "mode": bool(user.get("mode", True)), + "layout": str(user.get("layout", "new")), + "debug": bool(user.get("debug", False)), + "sfx": bool(user.get("sfx", True)), + "bgm": bool(user.get("bgm", True)), + "bgm_song": int(user.get("bgm_song", 2)), + "pfp_data": int(user.get("pfp_data", 1)), + "quote": str(user.get("quote", "")) + }) + except Exception as e: + logging.error(f"Failed to migrate user {username}: {str(e)}") + else: + usernames.add(username) + lower_usernames.add(username.lower()) + del lower_usernames + except Exception as e: + logging.error(f"Failed to migrate users: {str(e)}") + + # Migrate chats + try: + logging.info("Migrating chats...") + chat_ids = set() + for chat_id in os.listdir("./Meower/Storage/Chats/Indexes"): + try: + f = open(f"./Meower/Storage/Chats/Indexes/{chat_id}", "r") + chat = ujson.load(f) + f.close() + if chat["owner"] not in usernames: + continue + db.chats.insert_one({ + "_id": chat_id, + "nickname": chat["nickname"], + "owner": chat["owner"], + "members": [], + "invite_code": secrets.token_urlsafe(5), + "created": int(time.time()) + }) + except Exception as e: + logging.error(f"Failed to migrate chat {chat_id}: {str(e)}") + else: + chat_ids.add(chat_id) + except Exception as e: + logging.error(f"Failed to migrate chats: {str(e)}") + + # Migrate chat members + try: + logging.info("Migrating chat members...") + for username in os.listdir("./Meower/Storage/Chats/UserIndexes"): + try: + for chat_name in os.listdir(f"./Meower/Storage/Chats/UserIndexes/{username}"): + try: + f = open(f"./Meower/Storage/Chats/UserIndexes/{username}/{chat_name}", "r") + membership = ujson.load(f) + f.close() + db.chats.update_one({"_id": membership["chat_uuid"]}, {"$addToSet": {"members": username}}) + except Exception as e: + logging.error(f"Failed to migrate chat membership for {username} on chat {chat_name}: {str(e)}") + except Exception as e: + logging.error(f"Failed to migrate chat memberships for {username}: {str(e)}") + except Exception as e: + logging.error(f"Failed to migrate chat members: {str(e)}") + + # Migrate home posts + try: + logging.info("Migrating home posts...") + for post_id in os.listdir("./Meower/Storage/Categories/Home/Messages"): + try: + f = open(f"./Meower/Storage/Categories/Home/Messages/{post_id}", "r") + post = ujson.load(f) + f.close() + ts = post["t"] + ts = datetime(year=int(ts["y"]), month=int(ts["mo"]), day=int(ts["d"]), + hour=int(ts["h"]), minute=int(ts["mi"]), second=int(ts["s"])) + db.posts.insert_one({ + "_id": uid.uuid(), + "type": 1, + "origin": "home", + "author": str(post["u"]), + "content": str(post["p"]), + "time": int(ts.timestamp()), + "deleted_at": (int(time.time()) if post["isDeleted"] else None) + }) + except Exception as e: + logging.error(f"Failed to migrate home post {post_id}: {str(e)}") + except Exception as e: + logging.error(f"Failed to migrate home posts: {str(e)}") + + # Migrate chat messages + try: + logging.info("Migrating chat messages...") + for message_id in os.listdir("./Meower/Storage/Chats/Messages"): + try: + f = open(f"./Meower/Storage/Chats/Messages/{message_id}", "r") + message = ujson.load(f) + f.close() + ts = message["t"] + ts = datetime(year=int(ts["y"]), month=int(ts["mo"]), day=int(ts["d"]), + hour=int(ts["h"]), minute=int(ts["mi"]), second=int(ts["s"])) + db.posts.insert_one({ + "_id": uid.uuid(), + "type": 1, + "origin": str(message["chatid"]), + "author": str(message["u"]), + "content": str(message["p"]), + "time": int(ts.timestamp()) + }) + except Exception as e: + logging.error(f"Failed to migrate chat message {message_id}: {str(e)}") + except Exception as e: + logging.error(f"Failed to migrate chat messages: {str(e)}") + + # Migrate IP bans + try: + logging.info("Migrating IP bans...") + f = open(f"./Meower/Jail/IPBanlist.json", "r") + ip_bans = ujson.load(f) + f.close() + for ip_address in ip_bans["wildcard"]: + try: + if ip_address == "127.0.0.1": + continue + db.netlog.insert_one({ + "_id": ip_address, + "banned": True + }) + except Exception as e: + logging.error(f"Failed to migrate IP ban of {ip_address}: {str(e)}") + for username, ip_address in ip_bans["users"].items(): + try: + db.users.update_one({"_id": username}, {"$set": {"last_ip": ip_address}}) + db.netlog.update_one({"_id": ip_address}, { + "$addToSet": {"users": username}, + "$set": {"last_user": username} + }) + except: + pass + except Exception as e: + logging.error(f"Failed to migrate IP bans: {str(e)}") + + # Migrate filter + try: + logging.info("Migrating filter config...") + f = open(f"./Meower/Config/filter.json", "r") + filter_config = ujson.load(f) + f.close() + db.config.insert_one({ + "_id": "filter", + "whitelist": filter_config["whitelist"], + "blacklist": filter_config["blacklist"] + }) + except Exception as e: + logging.error(f"Failed to migrate filter config: {str(e)}") + + +def migrate_from_v1(db): + # Migrate users + try: + logging.info("Migrating users...") + users = list(db.usersv0.find({})) + usernames = [] + lower_usernames = set() + for i, user in enumerate(copy(users)): + username = str(user.get("_id")) + try: + if username.lower() in lower_usernames: + raise Exception("Duplicate username") + users[i] = { + "_id": str(username), + "lower_username": str(username.lower()), + "uuid": str(user.get("uuid", uid.uuid())), + "created": int(user.get("created", int(time.time()))), + "pswd": str(user.get("pswd")), + "lvl": int(user.get("lvl", 0)), + "last_ip": user.get("last_ip"), + "banned_until": (-1 if user.get("banned") else None), + "unread_inbox": bool(user.get("unread_inbox", False)), + "theme": str(user.get("theme", "orange")), + "mode": bool(user.get("mode", True)), + "layout": str(user.get("layout", "new")), + "debug": bool(user.get("debug", False)), + "sfx": bool(user.get("sfx", True)), + "bgm": bool(user.get("bgm", True)), + "bgm_song": int(user.get("bgm_song", 2)), + "pfp_data": int(user.get("pfp_data", 1)), + "quote": str(user.get("quote", "")) + } + except Exception as e: + logging.error(f"Failed to migrate user {username}: {str(e)}") + users[i] = {} + else: + usernames.append(username) + lower_usernames.add(username.lower()) + del lower_usernames + db.users.insert_many(users) + db.users.delete_many({"lower_username": {"$exists": False}}) + db.usersv0.drop() + db.usersv1.drop() + except Exception as e: + logging.error(f"Failed to migrate users: {str(e)}") + + # Migrate chats + try: + logging.info("Migrating chats...") + chats = list(db.chats.find({})) + chat_ids = [] + for i, chat in enumerate(copy(chats)): + chat_id = str(chat.get("_id")) + try: + if chat["owner"] not in usernames: + raise Exception("Chat owner no longer exists") + if len(chat["members"]) == 0: + raise Exception("Members list is empty") + for j, username in enumerate(chat["members"]): + if username not in usernames: + del chat["members"][j] + chat["members"] = list(dict.fromkeys(chat["members"])) + chat["invite_code"] = secrets.token_urlsafe(5) + chat["created"] = int(time.time()) + chats[i] = chat + except Exception as e: + logging.error(f"Failed to migrate chat {chat_id}: {str(e)}") + chats[i] = {} + else: + chat_ids.append(chat_id) + db.chats.drop() + db.chats.insert_many(chats) + db.chats.delete_many({"nickname": {"$exists": False}}) + except Exception as e: + logging.error(f"Failed to migrate chats: {str(e)}") + + # Migrate posts + try: + logging.info("Migrating posts...") + db.posts.delete_many({"$or": [ + { + "post_origin": {"$nin": (["home"] + chat_ids)} + }, + { + "u": {"$nin": usernames} + } + ]}) + db.posts.update_many({"isDeleted": True}, {"$set": {"deleted_at": int(time.time())}}) + db.posts.update_many({}, [{"$set": {"time": "$t.e"}}]) + db.posts.update_many({}, { + "$rename": { + "post_origin": "origin", + "u": "author", + "p": "content", + "unfiltered_p": "unfiltered_content" + }, + "$unset": { + "t": "", + "post_id": "", + "isDeleted": "" + } + }) + except Exception as e: + logging.error(f"Failed to migrate posts: {str(e)}") + + # Migrate reports + try: + logging.info("Migrating reports...") + db.reports.update_many({}, {"$set": { + "score": 0, + "created": int(time.time()) + }}) + except Exception as e: + logging.error(f"Failed to migrate reports: {str(e)}") + + # Migrate IP bans + try: + logging.info("Migrating IP bans...") + ip_bans = db.config.find_one({"_id": "IPBanlist"}) + db.netlog.update_many({"_id": {"$in": ip_bans.get("wildcard", [])}}, {"$set": { + "banned": True + }}) + except Exception as e: + logging.error(f"Failed to migrate IP bans: {str(e)}") + + # Migrate status + try: + logging.info("Migrating status...") + status = db.config.find_one({"_id": "status"}) + if status.get("repair_mode"): + events.redis.set("repair_mode", "") # really bad solution to circulr import error, but it works + except Exception as e: + logging.error(f"Failed to migrate status: {str(e)}") + + # Clear unnecessary config items + try: + logging.info("Clearing unnecessary config items...") + db.config.delete_many({"_id": {"$nin": ["filter"]}}) + except Exception as e: + logging.error(f"Failed to clear unnecessary config items: {str(e)}") diff --git a/src/common/util/profanity.py b/src/common/util/profanity.py new file mode 100644 index 0000000..61c63ce --- /dev/null +++ b/src/common/util/profanity.py @@ -0,0 +1,15 @@ +from better_profanity import profanity + +from src.common.database import db + + +# Load custom filter settings +custom_settings = db.config.find_one({"_id": "filter"}) + + +def censor(text: str): + profanity.load_censor_words(whitelist_words=custom_settings["whitelist"]) + text = profanity.censor(text) + profanity.load_censor_words(whitelist_words=custom_settings["whitelist"], custom_words=custom_settings["blacklist"]) + text = profanity.censor(text) + return text diff --git a/src/common/util/regex.py b/src/common/util/regex.py new file mode 100644 index 0000000..e9607c1 --- /dev/null +++ b/src/common/util/regex.py @@ -0,0 +1 @@ +USERNAME_VALIDATION = "[a-zA-Z0-9-_.]{1,20}" \ No newline at end of file diff --git a/src/common/util/security.py b/src/common/util/security.py new file mode 100644 index 0000000..a4696c0 --- /dev/null +++ b/src/common/util/security.py @@ -0,0 +1,56 @@ +from base64 import b64encode, b64decode +from hashlib import sha512 +import hmac + +from src.common.util import errors +from src.common.database import db, redis + + +# Get signing key +SIGNING_KEY = db.config.find_one({"_id": "security"})["signing_key"] + + +def sign_data(data: bytes): + return b64encode(hmac.new(key=SIGNING_KEY, msg=data, digestmod=sha512).digest()) + + +def validate_signature(signature: bytes, data: bytes): + return hmac.compare_digest(b64decode(signature), hmac.new(key=SIGNING_KEY, msg=data, digestmod=sha512).digest()) + + +def check_ratelimit(identifier: str, bucket: str): + # Construct ratelimit key + key = f"rtlm:{identifier}:{bucket}" + + # Get amount remaining + try: + remaining = int(redis.get(key).decode()) + except: + remaining = 1 + + # Return whether remaining is above 0 + return (remaining <= 0) + + +def ratelimit(identifier: str, bucket: str, limit: int, expires: int): + # Construct ratelimit key + key = f"rtlm:{identifier}:{bucket}" + + # Get amount remaining and expiration + try: + remaining = int(redis.get(key).decode()) + except: + remaining = limit + + # Set new remaining amount + remaining -= 1 + redis.set(key, remaining, ex=expires) + + +def auto_ratelimit(identifier: str, bucket: str, limit: int, expires: int): + # Check if client is ratelimited + if check_ratelimit(identifier, bucket): + raise errors.Ratelimited + + # Ratelimit client + ratelimit(identifier, bucket, limit, expires) diff --git a/src/common/util/uid.py b/src/common/util/uid.py new file mode 100644 index 0000000..d7b2ac0 --- /dev/null +++ b/src/common/util/uid.py @@ -0,0 +1,25 @@ +from datetime import datetime, timezone +from uuid import uuid4 + + +def timestamp(epoch: int = None, jsonify: bool = False) -> dict|datetime: + if epoch: + dt = datetime.fromtimestamp(epoch, tz=(timezone.utc if jsonify else None)) + else: + dt = datetime.now(tz=(timezone.utc if jsonify else None)) + + if jsonify: + return { + "mo": dt.strftime("%m"), + "d": dt.strftime("%d"), + "y": dt.strftime("%Y"), + "h": dt.strftime("%H"), + "mi": dt.strftime("%M"), + "s": dt.strftime("%S"), + "e": int(dt.timestamp()) + } + else: + return dt + +def uuid() -> str: + return str(uuid4()) diff --git a/start_server.sh b/start_server.sh new file mode 100644 index 0000000..2fc4a1a --- /dev/null +++ b/start_server.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +cmds=("python run_api.py" "python run_cl3.py" "python background_worker.py") + +for cmd in "${cmds[@]}"; do { + echo "Process \"$cmd\" started"; + $cmd & pid=$! + PID_LIST+=" $pid"; + sleep 2 +} done + +trap "kill $PID_LIST" SIGINT + +echo "Parallel processes have started"; + +wait $PID_LIST + +echo +echo "All processes have completed"; \ No newline at end of file diff --git a/supporter.py b/supporter.py deleted file mode 100644 index 92e0050..0000000 --- a/supporter.py +++ /dev/null @@ -1,341 +0,0 @@ -from datetime import datetime -from better_profanity import profanity -import time -import traceback -import sys -import string -from threading import Thread - -""" - -Meower Supporter Module - -This module provides logging, error traceback, and other miscellaneous supportive functionality. -This keeps the main.py clean and more understandable. - -""" - -class Supporter: - def __init__(self, cl=None, packet_callback=None): - self.filter = None - self.last_packet = dict() - self.burst_amount = dict() - self.ratelimits = dict() - self.good_ips = set() - self.known_vpns = set() - self.status = {"repair_mode": True, "is_deprecated": False} - self.cl = cl - self.profanity = profanity - self.packet_handler = packet_callback - self.listener_detected = False - self.listener_id = None - - if not self.cl == None: - # Add custom status codes to CloudLink - self.cl.codes["KeyNotFound"] = "I:010 | Key Not Found" - self.cl.codes["PasswordInvalid"] = "I:011 | Invalid Password" - self.cl.codes["GettingReady"] = "I:012 | Getting ready" - self.cl.codes["ObsoleteClient"] = "I:013 | Client is out-of-date or unsupported" - self.cl.codes["Pong"] = "I:014 | Pong" - self.cl.codes["IDExists"] = "I:015 | Account exists" - self.cl.codes["2FAOnly"] = "I:016 | 2FA Required" - self.cl.codes["MissingPermissions"] = "I:017 | Missing permissions" - self.cl.codes["Banned"] = "E:018 | Account Banned" - self.cl.codes["IllegalChars"] = "E:019 | Illegal characters detected" - self.cl.codes["Kicked"] = "E:020 | Kicked" - self.cl.codes["ChatExists"] = "E:021 | Chat exists" - self.cl.codes["ChatNotFound"] = "E:022 | Chat not found" - self.cl.codes["ChatFull"] = "E:023 | Chat full" - - # Create permitted lists of characters - self.permitted_chars_username = [] - self.permitted_chars_post = [] - for char in string.ascii_letters: - self.permitted_chars_username.append(char) - self.permitted_chars_post.append(char) - for char in string.digits: - self.permitted_chars_username.append(char) - self.permitted_chars_post.append(char) - for char in string.punctuation: - self.permitted_chars_post.append(char) - self.permitted_chars_username.extend(["_", "-"]) - self.permitted_chars_post.append(" ") - - # Peak number of users logger - self.peak_users_logger = { - "count": 0, - "timestamp": { - "mo": 0, - "d": 0, - "y": 0, - "h": 0, - "mi": 0, - "s": 0, - "e": 0 - } - } - - if not self.cl == None: - # Specify server callbacks - self.cl.callback("on_packet", self.on_packet) - self.cl.callback("on_close", self.on_close) - self.cl.callback("on_connect", self.on_connect) - - self.log("Supporter initialized!") - - def full_stack(self): - exc = sys.exc_info()[0] - if exc is not None: - f = sys.exc_info()[-1].tb_frame.f_back - stack = traceback.extract_stack(f) - else: - stack = traceback.extract_stack()[:-1] - trc = 'Traceback (most recent call last):\n' - stackstr = trc + ''.join(traceback.format_list(stack)) - if exc is not None: - stackstr += ' ' + traceback.format_exc().lstrip(trc) - return stackstr - - def log(self, event): - print("{0}: {1}".format(self.timestamp(4), event)) - - def sendPacket(self, payload, listener_detected=False, listener_id=None): - if not self.cl == None: - if listener_detected: - if "id" in payload: - payload["listener"] = listener_id - self.cl.sendPacket(payload) - else: - self.cl.sendPacket(payload) - - def get_client_statedata(self, client): # "steals" information from the CloudLink module to get better client data - if not self.cl == None: - if type(client) == str: - client = self.cl._get_obj_of_username(client) - if not client == None: - if client['id'] in self.cl.statedata["ulist"]["objs"]: - tmp = self.cl.statedata["ulist"]["objs"][client['id']] - return tmp - else: - return None - - def modify_client_statedata(self, client, key, newvalue): # WARN: Use with caution: DO NOT DELETE UNNECESSARY KEYS! - if not self.cl == None: - if type(client) == str: - client = self.cl._get_obj_of_username(client) - if not client == None: - if client['id'] in self.cl.statedata["ulist"]["objs"]: - try: - self.cl.statedata["ulist"]["objs"][client['id']][key] = newvalue - return True - except: - self.log("{0}".format(self.full_stack())) - return False - else: - return False - - def delete_client_statedata(self, client, key): # WARN: Use with caution: DO NOT DELETE UNNECESSARY KEYS! - if not self.cl == None: - if type(client) == str: - client = self.cl._get_obj_of_username(client) - if not client == None: - if client['id'] in self.cl.statedata["ulist"]["objs"]: - if key in self.cl.statedata["ulist"]["objs"][client['id']]: - try: - del self.cl.statedata["ulist"]["objs"][client['id']][key] - return True - except: - self.log("{0}".format(self.full_stack())) - return False - else: - return False - - def log_peak_users(self): - if not self.cl == None: - current_users = len(self.cl.getUsernames()) - if current_users > self.peak_users_logger["count"]: - today = datetime.now() - self.peak_users_logger = { - "count": current_users, - "timestamp": self.timestamp(1) - } - self.log("New peak in # of concurrent users: {0}".format(current_users)) - #self.create_system_message("Yay! New peak in # of concurrent users: {0}".format(current_users)) - payload = { - "mode": "peak", - "payload": self.peak_users_logger - } - self.sendPacket({"cmd": "direct", "val": payload}) - - def on_close(self, client): - if not self.cl == None: - if type(client) == dict: - self.log("{0} Disconnected.".format(client["id"])) - elif type(client) == str: - self.log("{0} Logged out.".format(self.cl._get_username_of_obj(client))) - self.log_peak_users() - - def on_connect(self, client): - if not self.cl == None: - if self.status["repair_mode"]: - self.log("Refusing connection from {0} due to repair mode being enabled".format(client["id"])) - self.cl.kickClient(client) - else: - self.log("{0} Connected.".format(client["id"])) - self.modify_client_statedata(client, "authtype", "") - self.modify_client_statedata(client, "authed", False) - - # Rate limiter - self.modify_client_statedata(client, "last_packet", 0) - - def on_packet(self, message): - if not self.cl == None: - # CL Turbo Support - self.listener_detected = ("listener" in message) - self.listener_id = None - - if self.listener_detected: - self.listener_id = message["listener"] - - # Read packet contents - id = message["id"] - val = message["val"] - clienttype = None - client = message["id"] - if type(message["id"]) == dict: - ip = self.cl.getIPofObject(client) - clienttype = 0 - elif type(message["id"]) == str: - ip = self.cl.getIPofUsername(client) - clienttype = 1 - - # Handle packet - cmd = None - if "cmd" in message: - cmd = message["cmd"] - - if not self.packet_handler == None: - self.packet_handler(cmd, ip, val, self.listener_detected, self.listener_id, client, clienttype) - - def timestamp(self, ttype): - today = datetime.now() - if ttype == 1: - return { - "mo": (datetime.now()).strftime("%m"), - "d": (datetime.now()).strftime("%d"), - "y": (datetime.now()).strftime("%Y"), - "h": (datetime.now()).strftime("%H"), - "mi": (datetime.now()).strftime("%M"), - "s": (datetime.now()).strftime("%S"), - "e": (int(time.time())) - } - elif ttype == 2: - return str(today.strftime("%H%M%S")) - elif ttype == 3: - return str(today.strftime("%d%m%Y%H%M%S")) - elif ttype == 4: - return today.strftime("%m/%d/%Y %H:%M.%S") - elif ttype == 5: - return today.strftime("%d%m%Y") - - def ratelimit(self, client): - # Rate limiter - self.modify_client_statedata(client, "last_packet", int(time.time())) - - def wordfilter(self, message): - # Word censor - if self.filter != None: - self.profanity.load_censor_words(whitelist_words=self.filter["whitelist"]) - message = self.profanity.censor(message) - self.profanity.load_censor_words(whitelist_words=self.filter["whitelist"], custom_words=self.filter["blacklist"]) - message = self.profanity.censor(message) - else: - self.log("Failed loading profanity filter : Using default filter as fallback") - self.profanity.load_censor_words() - message = self.profanity.censor(message) - return message - - def isAuthenticated(self, client): - if not self.cl == None: - return self.get_client_statedata(client)["authed"] - - def setAuthenticatedState(self, client, value): - if not self.cl == None: - self.modify_client_statedata(client, "authed", value) - - def checkForBadCharsUsername(self, value): - # Check for profanity in username, will return '*' if there's profanity which will be blocked as an illegal character - value = self.wordfilter(value) - - badchars = False - for char in value: - if not char in self.permitted_chars_username: - badchars = True - break - return badchars - - def checkForBadCharsPost(self, value): - badchars = False - for char in value: - if not char in self.permitted_chars_post: - badchars = True - break - return badchars - - def autoID(self, client, username): - if not self.cl == None: - # really janky code that automatically sets user ID - self.modify_client_statedata(client, "username", username) - self.cl.statedata["ulist"]["usernames"][username] = client["id"] - self.sendPacket({"cmd": "ulist", "val": self.cl._get_ulist()}) - self.log("{0} autoID given".format(username)) - - def kickUser(self, username, status="Kicked"): - if not self.cl == None: - if username in self.cl.getUsernames(): - self.log("Kicking {0}".format(username)) - - # Tell client it's going to get kicked - self.sendPacket({"cmd": "direct", "val": self.cl.codes[status], "id": username}) - - # Unauthenticate client - client = self.cl.statedata["ulist"]["objs"][self.cl.statedata["ulist"]["usernames"][username]]["object"] - self.cl._closed_connection_server(client, None) - self.sendPacket({"cmd": "ulist", "val": self.cl._get_ulist()}) - - # Thread final closing - def run(client): - time.sleep(1) - client["handler"].send_close(1000, bytes('', encoding='utf-8')) - Thread(target=run, args=(client,)).start() - - def check_for_spam(self, type, client, burst=1, seconds=1): - # Check if type and client are in ratelimit dictionary - if not (type in self.last_packet): - self.last_packet[type] = {} - self.burst_amount[type] = {} - self.ratelimits[type] = {} - if client not in self.last_packet[type]: - self.last_packet[type][client] = 0 - self.burst_amount[type][client] = 0 - self.ratelimits[type][client] = 0 - - # Check if user is currently ratelimited - if self.ratelimits[type][client] > time.time(): - return True - - # Check if max burst has expired - if (self.last_packet[type][client] + seconds) < time.time(): - self.burst_amount[type][client] = 0 - - # Set last packet time and add to burst amount - self.last_packet[type][client] = time.time() - self.burst_amount[type][client] += 1 - - # Check if burst amount is over max burst - if self.burst_amount[type][client] > burst: - self.ratelimits[type][client] = (time.time() + seconds) - self.burst_amount[type][client] = 0 - return True - else: - return False \ No newline at end of file