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"
"
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)
Social Links
' for i in social: if SOCIAL_INFO[i[1]]["link"]: inner += f"