# MAKE SURE YOU INSTALL ALL NEEDED LIBRARIES! # pip install flask dotindex ensure-file 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 ensure_file import ensure_file 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 FLAGS = { "agender": "Agender", "ally": "Ally", "aroace": "Aroace", "aro": "Aromantic", "ace": "Asexual", "bicurious": "Bicurious", "bigender": "Bigender", "bi": "Bisexual", "cisgender": "Cisgender", "demiboy": "Demiboy", "demigirl": "Demigirl", "demiromantic": "Demiromantic", "demisexual": "Demisexual", "gay": "Gay (Rainbow)", "gayman": "Gay Man", "genderfluid": "Genderfluid", "intersex": "Intersex", "lesbian": "Lesbian", "nonbinary": "Nonbinary", "omnigender": "Omnigender", "pan": "Pansexual", "polyamory": "Polyamorous", "straight": "Straight", "transfem": "Transfeminine", "trans": "Transgender", "transmasc": "Transmasculine" } 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=False) -> 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 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"); if "social" in json and len(json.social): inner += '

Social Links

' for i in json.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 += "
" if "flags" in json and len(json.flags): inner += '

Pride Flags

' for i in json.flags: inner += f'' inner += "
" inner += f'
Key:
{icons["4"]} - Great
{icons["3"]} - Good
{icons["2"]} - Okay
{icons["1"]} - Bad
' 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, True) if "flags" in x: flags = {} for i in x["flags"]: if i in FLAGS: flags[i] = None user_data["flags"] = sorted([i for i in flags]) 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("/css/")(create_folder_serve("css")) app.route("/img/flags/")(create_folder_serve("img/flags")) app.route("/js/")(create_folder_serve("js")) 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)