CONTENT_DIRECTORY = "./public/" SAVING_DIRECTORY = "./save/" UPGRADE_TO_HTTPS = False import hashlib import random import shutil import flask import json import os import re from DotIndex import DotIndex from typing import Union, Callable from flask import request, redirect from werkzeug.middleware.proxy_fix import ProxyFix app = flask.Flask(__name__) app.url_map.strict_slashes = False SOCIALS_REGEX = { "discord" : re.compile(r"^(?!.*\.\.)(?=.{2,32}$)[a-z0-9_.]+$"), "twitter" : re.compile(r"^(?!.*twitter)(?!.*admin)[a-z0-9_]{1,15}$", re.IGNORECASE), "github" : re.compile(r"^(?!.*--)[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$", re.IGNORECASE), "twitch" : re.compile(r"^[a-z0-9_]{4,25}$", re.IGNORECASE), "reddit" : re.compile(r"^[a-z0-9_-]{3,20}$", re.IGNORECASE), "snapchat" : re.compile(r"^(?=.{3,15}$)[a-z0-9]+(?:[_.-][a-z0-9]+)?$", re.IGNORECASE), "instagram" : re.compile(r"^[a-z0-9_.]{1,30}$", re.IGNORECASE), "facebook" : re.compile(r"^([a-z0-9].*){1,50}$", re.IGNORECASE), "tiktok" : re.compile(r"^[a-z0-9_.]{1,25}$", re.IGNORECASE), "smiggins" : re.compile(r"^[a-z0-9_-]{1,18}$"), "tringl" : re.compile(r"^[a-z0-9_]{1,24}$") } SOCIAL_ICONS = { "discord": '', "facebook": '', "github": '', "instagram": '', "reddit": '', "smiggins": '', "snapchat": '', "tiktok": '', "tringl": '', "twitch": '', "twitter": '' } SOCIAL_INFO = { "discord": { "link": None, "prefix": "" }, "facebook": { "link": "https://www.facebook.com/%q", "prefix": "" }, "github": { "link": "https://github.com/%q", "prefix": "" }, "instagram": { "link": "https://www.instagram.com/%q/", "prefix": "" }, "reddit": { "link": "https://www.reddit.com/u/%q", "prefix": "/u/" }, "smiggins": { "link": "https://trinkey.pythonanywhere.com/u/%q", "prefix": "@" }, "snapchat": { "link": "https://www.snapchat.com/add/%q", "prefix": "" }, "tiktok": { "link": "https://www.tiktok.com/@%q", "prefix": "" }, "tringl": { "link": "https://ngl.pythonanywhere.com/m/%q", "prefix": "@" }, "twitch": { "link": "https://www.twitch.tv/%q", "prefix": "" }, "twitter": { "link": "https://twitter.com/%q", "prefix": "@" } } def validate_color(color: str) -> bool: if len(color) != 7 or color[0] != "#": return False for i in color[1::]: if i not in "abcdef0123456789": return False return True def sort_list(l: list[list[str]], alphabetical=True) -> list[list[str]]: output = [] for i in l: if i[0] and (not alphabetical and (i[1] in ["1", "2", "3", "4"]) or (alphabetical and i[1])): output.append(i) if alphabetical: return sorted(output, key=lambda x: x[1] + x[0]) return sorted(output, key=lambda x: {"1": "d", "2": "c", "3": "b", "4": "a"}[x[1]] + x[0]) def return_dynamic_content_type(content: Union[str, bytes], content_type: str="text/html") -> flask.Response: response = flask.make_response(content) response.headers["Content-Type"] = content_type return response def sha(string: Union[str, bytes]) -> str: if type(string) == str: return hashlib.sha256(str.encode(string)).hexdigest() elif type(string) == bytes: return hashlib.sha256(string).hexdigest() return "" def ensure_file(path: str, *, default_value: str="", folder: bool=False) -> None: if os.path.exists(path): if folder and not os.path.isdir(path): os.remove(path) os.makedirs(path) elif not folder and os.path.isdir(path): shutil.rmtree(path, ignore_errors=True) f = open(path, "w") f.write(default_value) f.close() else: if folder: os.makedirs(path) else: f = open(path, "w") f.write(default_value) f.close() def escape_html(string: str) -> str: return string.replace("&", "&").replace("<", "<").replace("\"", """) def generate_token(username: str, passhash: str) -> str: return sha(sha(f"{username}:{passhash}") + "among us in real life, sus, sus") def list_public( sort: str="random", page: int=0, limit: int=25 ) -> dict: # Sort: "alphabetical", "random" # Page: for "alphabetical", page for next. 0 is first page, 1 is second... x = json.loads(open(f"{SAVING_DIRECTORY}public/list.json", "r").read()) output = { "end": True, "list": [] } if sort == "alphabetical": x = x[limit * page::] output["end"] = len(x) <= limit for i in x[:limit:]: q = json.loads(open(f"{SAVING_DIRECTORY}{i}.json", "r").read()) output["list"].append({ "colors": q["colors"], "display_name": q["display_name"], "bio": q["description"], "username": i }) elif sort == "random": random.shuffle(x) for i in x[:limit:]: q = json.loads(open(f"{SAVING_DIRECTORY}{i}.json", "r").read()) output["list"].append({ "colors": q["colors"], "display_name": q["display_name"], "bio": q["description"], "username": i }) return output def create_file_serve(file) -> Callable: x = lambda property=None: flask.send_file(f"{CONTENT_DIRECTORY}{file}") x.__name__ = file return x def create_folder_serve(directory) -> Callable: x = lambda file: flask.send_from_directory(f"{CONTENT_DIRECTORY}{directory}", file) x.__name__ = directory return x def get_template(json, username): def add_to_output(starting, json, key, title): starting += f'

{title}

' for i in json[key]: starting += f"""
{icons[i[1]]} {escape_html(i[0])}
""" return starting + "
" json = DotIndex(json) icons = { "1": '', "2": '', "3": '', "4": '' } styles = f"--background: {json.colors.background}; --background-low-opacity: {json.colors.background}33; --accent: {json.colors.accent}; --accent-low-opacity: {json.colors.accent}66; --text: {json.colors.text}; --text-low-opacity: {json.colors.text}88;" # type: ignore title = f"{escape_html(json.display_name)} (@{username})".replace("{{TEMPLATE}}", "HA" * 50) # type: ignore embed = f'' # type: ignore inner = f'

{escape_html(json.display_name)}

{escape_html(json.description)}
' # type: ignore inner = add_to_output(inner, json, "names", "Names"); inner = add_to_output(inner, json, "pronouns", "Pronouns"); inner = add_to_output(inner, json, "honorifics", "Honorifics"); inner = add_to_output(inner, json, "compliments", "Compliments"); inner = add_to_output(inner, json, "relationship", "Relationship
Descriptions"); try: social = json.social # type: ignore inner += '

Social Links

' for i in social: if SOCIAL_INFO[i[1]]["link"]: inner += f"
{SOCIAL_ICONS[i[1]]} {SOCIAL_INFO[i[1]]['prefix']}{escape_html(i[0])}
" else: inner += f"
{SOCIAL_ICONS[i[1]]} {SOCIAL_INFO[i[1]]['prefix']}{escape_html(i[0])}
" inner += "
" except AttributeError as e: print(e) inner += "
" return title, inner, styles, embed def get_user_page(user): user = user.lower() x = open(f"{CONTENT_DIRECTORY}user.html", "r").read() try: user_json = json.loads(open(f"{SAVING_DIRECTORY}{user}.json", "r").read()) except FileNotFoundError: return x.replace("{{TEMPLATE}}", '

User not found!

Sign up - Log in').replace("{{TITLE}}", "User not found - InfoPage") title, inner, styles, embed = get_template(user_json, user) return x.replace(" 24 or len(username) < 1: flask.abort(400) for i in username: if i not in "abcdefghijklmnopqrstuvwxyz0123456789_-": return { "valid": False, "reason": "User doesn't exist." } try: open(f"{SAVING_DIRECTORY}{username}.json", "r") except FileNotFoundError: return { "valid": False, "reason": "User doesn't exist." } token = generate_token(username, passhash) try: enforced_username = open(f"{SAVING_DIRECTORY}tokens/{token}.txt", "r").read() except FileNotFoundError: return { "valid": False, "reason": "Invalid password" } if enforced_username != username: return { "valid": False, "reason": "Invalid password" } return { "valid": True, "token": token } def api_account_signup(): try: x = json.loads(request.data) username = x["username"].replace(" ", "").lower() passhash = x["password"] except json.JSONDecodeError: flask.abort(400) except KeyError: flask.abort(400) if len(x["username"]) > 24 or len(username) < 1: flask.abort(400) if len(passhash) != 64: flask.abort(400) for i in passhash: if i not in "abcdef0123456789": flask.abort(400) for i in username: if i not in "abcdefghijklmnopqrstuvwxyz0123456789_-": return { "valid": False, "reason": "Username can only contain a-z, 0-9, underscores, and hyphens." } try: open(f"{SAVING_DIRECTORY}{username}.json", "r") return { "valid": False, "reason": "Username taken." } except FileNotFoundError: pass token = generate_token(username, passhash) ensure_file(f"{SAVING_DIRECTORY}tokens/{token}.txt", default_value=username) ensure_file(f"{SAVING_DIRECTORY}{username}.json", default_value=json.dumps({ "username": username, "display_name": x["username"], "description": "", "colors": { "accent": "#ff0000", "text": "#ffffff", "background": "#111122" }, "names": [ [x["username"], "4"] ], "pronouns": [ ["he/him", "3"], ["it/its", "3"], ["she/her", "3"], ["they/them", "3"] ], "honorifics": [ ["ma'am", "3"], ["madam", "3"], ["mir", "3"], ["mr.", "3"], ["ms.", "3"], ["mx.", "3"], ["sai", "3"], ["shazam", "3"], ["sir", "3"], ["zam", "3"] ], "compliments": [ ["cute", "3"], ["handsome", "3"], ["hot", "3"], ["pretty", "3"], ["sexy", "3"] ], "relationship": [ ["beloved", "3"], ["boyfriend", "3"], ["darling", "3"], ["enbyfriend", "3"], ["friend", "3"], ["girlfriend", "3"], ["husband", "3"], ["partner", "3"], ["wife", "3"] ], "public": False, "social": [] })) return return_dynamic_content_type(json.dumps({ "valid": True, "token": token })) def api_account_info_(user): try: return return_dynamic_content_type( open(f"{SAVING_DIRECTORY}{user}.json").read(), "application/json" ) except FileNotFoundError: flask.abort(404) def api_account_self(): try: return return_dynamic_content_type( open(SAVING_DIRECTORY + open(f'{SAVING_DIRECTORY}tokens/{request.cookies["token"]}.txt', 'r').read() + ".json", 'r').read(), "application/json" ) except FileNotFoundError: flask.abort(404) def api_account_change(): username = open(f'{SAVING_DIRECTORY}tokens/{request.cookies["token"]}.txt', 'r').read() x = json.loads(request.data) if generate_token(username, x["current"]) != request.cookies["token"]: flask.abort(401) new = generate_token(username, x["new"]) os.rename(f'{SAVING_DIRECTORY}tokens/{request.cookies["token"]}.txt', f'{SAVING_DIRECTORY}tokens/{new}.txt') return { "token": new } def api_account_delete(): username = open(f'{SAVING_DIRECTORY}tokens/{request.cookies["token"]}.txt', 'r').read() x = json.loads(request.data) token = generate_token(username, x["passhash"]) if generate_token(username, x["passhash"]) != request.cookies["token"]: flask.abort(401) os.remove(f"{SAVING_DIRECTORY}tokens/{token}.txt") os.remove(f"{SAVING_DIRECTORY}{username}.json") f = json.loads(open(f"{SAVING_DIRECTORY}public/list.json", "r").read()) if username in f: f.remove(username) g = open(f"{SAVING_DIRECTORY}public/list.json", "w") g.write(json.dumps(f)) g.close() return "200 OK" def api_save(): username = open(f'{SAVING_DIRECTORY}tokens/{request.cookies["token"]}.txt', 'r').read() x = json.loads(request.data) user_data = json.loads(open(f"{SAVING_DIRECTORY}{username}.json", "r").read()) if "display_name" in x and len(x["display_name"]) < 64 and len(x["display_name"]) > 0: user_data["display_name"] = x["display_name"] if "description" in x and len(x["description"]) < 512: user_data["description"] = x["description"] if "public" in x: user_data["public"] = bool(x["public"]) public_list = json.loads(open(f"{SAVING_DIRECTORY}public/list.json", "r").read()) if user_data["public"] and username not in public_list: public_list.append(username) elif not user_data["public"] and username in public_list: public_list.remove(username) f = open(f"{SAVING_DIRECTORY}public/list.json", "w") f.write(json.dumps(sorted(public_list))) f.close() if "colors" in x: if "accent" in x["colors"] and validate_color(x["colors"]["accent"]): user_data["colors"]["accent"] = x["colors"]["accent"] if "background" in x["colors"] and validate_color(x["colors"]["background"]): user_data["colors"]["background"] = x["colors"]["background"] if "text" in x["colors"] and validate_color(x["colors"]["text"]): user_data["colors"]["text"] = x["colors"]["text"] if "names" in x: names = [] for i in x["names"]: if len(i) == 2 and int(i[1]) in [1, 2, 3, 4] and len(i[0]) > 0 and len(i[0]) < 48: names.append(i) user_data["names"] = sort_list(names) if "pronouns" in x: pronouns = [] for i in x["pronouns"]: if len(i) == 2 and int(i[1]) in [1, 2, 3, 4] and len(i[0]) > 0 and len(i[0]) < 48: pronouns.append(i) user_data["pronouns"] = sort_list(pronouns) if "honorifics" in x: honorifics = [] for i in x["honorifics"]: if len(i) == 2 and int(i[1]) in [1, 2, 3, 4] and len(i[0]) > 0 and len(i[0]) < 48: honorifics.append(i) user_data["honorifics"] = sort_list(honorifics) if "compliments" in x: compliments = [] for i in x["compliments"]: if len(i) == 2 and int(i[1]) in [1, 2, 3, 4] and len(i[0]) > 0 and len(i[0]) < 48: compliments.append(i) user_data["compliments"] = sort_list(compliments) if "relationship" in x: relationship = [] for i in x["relationship"]: if len(i) == 2 and int(i[1]) in [1, 2, 3, 4] and len(i[0]) > 0 and len(i[0]) < 48: relationship.append(i) user_data["relationship"] = sort_list(relationship) if "social" in x: social = [] for i in x["social"]: if len(i) == 2 and i[1] in SOCIALS_REGEX and SOCIALS_REGEX[i[1]].match(i[0]): social.append(i) user_data["social"] = sort_list(social) f = open(f"{SAVING_DIRECTORY}{username}.json", "w") f.write(json.dumps(user_data)) f.close() return "200 OK" def api_browse(): return list_public( request.args.get("sort"), # type: ignore int(request.args.get("page")) if "page" in request.args else 0 # type: ignore ) def home(): if "token" not in request.cookies: return flask.send_file(f"{CONTENT_DIRECTORY}home.html") token = request.cookies['token'] if len(token) != 64: return flask.send_file(f"{CONTENT_DIRECTORY}home.html") for i in token: if i not in "abcdef0123456789": return flask.send_file(f"{CONTENT_DIRECTORY}home.html") try: username = open(f"{SAVING_DIRECTORY}tokens/{token}.txt", "r").read() user_info = json.loads(open(f"{SAVING_DIRECTORY}{username}.json", "r").read()) except FileNotFoundError: return flask.send_file(f"{CONTENT_DIRECTORY}home.html") x = open(f"{CONTENT_DIRECTORY}home.html", "r").read() x = x.replace("{{USERNAME}}", username) x = x.replace("{{DISPL_NAME}}", user_info["display_name"]) x = x.replace("{{PUBLIC}}", "true" if "public" in user_info and user_info["public"] else "false") x = x.replace("{{TOTAL}}", str(len(json.loads(open(f"{SAVING_DIRECTORY}public/list.json", "r").read())))) return return_dynamic_content_type(x, "text/html") ensure_file(SAVING_DIRECTORY, folder=True) ensure_file(f"{SAVING_DIRECTORY}tokens/", folder=True) ensure_file(f"{SAVING_DIRECTORY}public", folder=True) ensure_file(f"{SAVING_DIRECTORY}public/list.json", default_value="[]") app.route("/")(create_file_serve("index.html")) app.route("/login")(create_file_serve("login.html")) app.route("/signup")(create_file_serve("signup.html")) app.route("/logout")(create_file_serve("logout.html")) app.route("/browse")(create_file_serve("browse.html")) app.route("/settings")(create_file_serve("settings.html")) app.route("/editor")(create_file_serve("editor.html")) app.route("/u/")(get_user_page) app.route("/home")(home) app.route("/js/")(create_folder_serve("js")) app.route("/css/")(create_folder_serve("css")) app.route("/api/account/login", methods=["POST"])(api_account_login) app.route("/api/account/signup", methods=["POST"])(api_account_signup) app.route("/api/account/info/", methods=["GET"])(api_account_info_) app.route("/api/account/self", methods=["GET"])(api_account_self) app.route("/api/account/change", methods=["POST"])(api_account_change) app.route("/api/account/delete", methods=["DELETE"])(api_account_delete) app.route("/api/save", methods=["PATCH"])(api_save) app.route("/api/browse", methods=["GET"])(api_browse) app.errorhandler(404)(create_file_serve("404.html")) if UPGRADE_TO_HTTPS: app.wsgi_app = ProxyFix(app.wsgi_app) @app.before_request def enforce_https(): if not request.is_secure: url = request.url.replace('http://', 'https://', 1) return redirect(url, code=301) if __name__ == "__main__": app.run(debug=True, port=8080)