we are so back

This commit is contained in:
maaa 2024-04-29 22:36:42 +02:00
parent 8ca281b1d3
commit e6359db97d
13 changed files with 173 additions and 346 deletions

View File

@ -1,5 +1,7 @@
burgercat: burger social media burgercat: burger social media
now back again!
### self hosting: ### self hosting:
this guide assumes you have git, python3, and redis installed on your server this guide assumes you have git, python3, and redis installed on your server
@ -10,10 +12,6 @@ python init_db
python main python main
``` ```
### zero downtime restarts:
- launch new burgercat server
- close previous server
### contribution guidelines: ### contribution guidelines:
- please check that your PR does not break anything - please check that your PR does not break anything
- unless absolutely nessecary, avoid adding dependecies - unless absolutely nessecary, avoid adding dependecies

View File

@ -3,5 +3,4 @@ PORT = 8080
SECRET_KEY = placeholder SECRET_KEY = placeholder
UPLOAD_FOLDER = uploads UPLOAD_FOLDER = uploads
PASSWORD_REQUIREMENT = 12 PASSWORD_REQUIREMENT = 12
UPLOAD_LIMIT = 4 UPLOAD_LIMIT = 12
REDIS_URL = redis://localhost

4
hashpass.py Normal file
View File

@ -0,0 +1,4 @@
from werkzeug.security import generate_password_hash, check_password_hash
passwordthing = input("insert pass: ")
print(generate_password_hash(passwordthing))

210
main
View File

@ -9,15 +9,13 @@ import datetime
import socket import socket
import threading import threading
import subprocess import subprocess
import asyncio
from hypercorn.config import Config
from hypercorn.asyncio import serve
from itertools import groupby from itertools import groupby
from waitress import serve
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.middleware.proxy_fix import ProxyFix from quart import Quart, render_template, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request, jsonify, websocket
from flask import Flask, render_template, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_sse import sse
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
# read config file # read config file
@ -29,23 +27,10 @@ SECRET_KEY = config["config"]["SECRET_KEY"]
UPLOAD_FOLDER = config["config"]["UPLOAD_FOLDER"] UPLOAD_FOLDER = config["config"]["UPLOAD_FOLDER"]
UPLOAD_LIMIT = config["config"]["UPLOAD_LIMIT"] UPLOAD_LIMIT = config["config"]["UPLOAD_LIMIT"]
PASSWORD_REQUIREMENT = config["config"]["PASSWORD_REQUIREMENT"] PASSWORD_REQUIREMENT = config["config"]["PASSWORD_REQUIREMENT"]
REDIS_URL = config["config"]["REDIS_URL"]
app = Flask(__name__) app = Quart(__name__)
app.config["SECRET_KEY"] = SECRET_KEY app.config["SECRET_KEY"] = SECRET_KEY
app.config["REDIS_URL"] = REDIS_URL
app.config["MAX_CONTENT_LENGTH"] = int(UPLOAD_LIMIT) * 1000 * 1000 app.config["MAX_CONTENT_LENGTH"] = int(UPLOAD_LIMIT) * 1000 * 1000
app.register_blueprint(sse, url_prefix="/stream")
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
limiter = Limiter(
get_remote_address,
app = app,
default_limits = ["3 per second"],
storage_uri = "memory://",
strategy = "fixed-window"
)
if SECRET_KEY == "placeholder": if SECRET_KEY == "placeholder":
print("[WARNING] Secret key is not set") print("[WARNING] Secret key is not set")
@ -136,15 +121,14 @@ def get_session(id):
full_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip() full_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
short_hash = subprocess.check_output(["git", "rev-parse", "--short=8", "HEAD"]).decode().strip() short_hash = subprocess.check_output(["git", "rev-parse", "--short=8", "HEAD"]).decode().strip()
ALLOWED_EXTENSIONS = {"png", "apng", "jpg", "jpeg", "gif", "svg", "webp"} ALLOWED_EXTENSIONS = {"png", "apng", "jpg", "jpeg", "gif", "svg", "webp", "jxl"}
def allowed_file(filename): def allowed_file(filename):
return '.' in filename and \ return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route("/", methods=("GET", "POST")) @app.route("/", methods=("GET", "POST"))
def main(): async def main():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
conn = get_db_connection() conn = get_db_connection()
posts = conn.execute("SELECT * FROM posts ORDER BY created DESC;").fetchall() posts = conn.execute("SELECT * FROM posts ORDER BY created DESC;").fetchall()
@ -153,22 +137,22 @@ def main():
if usersession: if usersession:
userCookie = get_session(usersession) userCookie = get_session(usersession)
user = get_user(userCookie["id"]) user = get_user(userCookie["id"])
return render_template("main.html", userdata=user, posts=posts, full_hash=full_hash, short_hash=short_hash) return await render_template("main.html", userdata=user, posts=posts, full_hash=full_hash, short_hash=short_hash)
else: else:
return render_template("main.html", posts=posts, full_hash=full_hash, short_hash=short_hash) return await render_template("main.html", posts=posts, full_hash=full_hash, short_hash=short_hash)
@app.route("/chat", methods=("GET", "POST")) @app.route("/chat", methods=("GET", "POST"))
def chat(): async def chat():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
userCookie = get_session(usersession) userCookie = get_session(usersession)
user = get_user(userCookie["id"]) user = get_user(userCookie["id"])
return render_template("chat.html", userdata=user) return await render_template("chat.html", userdata=user)
else: else:
return render_template("chat.html") return await render_template("chat.html")
@app.route("/api/chat/listrooms") @app.route("/api/chat/listrooms")
def chatlistrooms(): async def chatlistrooms():
conn = get_db_connection() conn = get_db_connection()
rooms = conn.execute("SELECT * FROM chatrooms ORDER BY id ASC;").fetchall() rooms = conn.execute("SELECT * FROM chatrooms ORDER BY id ASC;").fetchall()
conn.close() conn.close()
@ -185,8 +169,7 @@ def chatlistrooms():
return(template), 200 return(template), 200
@app.route("/api/chat/getmessages/<roomid>") @app.route("/api/chat/getmessages/<roomid>")
@limiter.exempt async def chatget(roomid):
def chatget(roomid):
messages = get_messages(roomid, 150) messages = get_messages(roomid, 150)
template = [] template = []
@ -209,13 +192,14 @@ def chatget(roomid):
return(template), 200 return(template), 200
@app.route("/api/chat/send/<roomid>", methods=("GET", "POST")) @app.route("/api/chat/send/<roomid>", methods=("GET", "POST"))
def chatsend(roomid): async def chatsend(roomid):
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
if request.method == "POST": if request.method == "POST":
data = request.get_json() data = await request.get_json()
content = data["content"] content = data["content"]
userCookie = get_session(usersession) userCookie = get_session(usersession)
@ -233,7 +217,7 @@ def chatsend(roomid):
"created": str(time.time()) "created": str(time.time())
} }
sse.publish({"message": chatMessageContent}, type="publish") #message_queue.append({"message": chatMessageContent})
conn = get_db_connection() conn = get_db_connection()
conn.execute("INSERT INTO chatmessages (content, chatroom_id, creator, created) VALUES (?, ?, ?, ?)", conn.execute("INSERT INTO chatmessages (content, chatroom_id, creator, created) VALUES (?, ?, ?, ?)",
@ -245,7 +229,7 @@ def chatsend(roomid):
@app.route("/@<pageusername>", methods=("GET", "POST")) @app.route("/@<pageusername>", methods=("GET", "POST"))
def user(pageusername): async def user(pageusername):
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
checkusername = check_username_taken(pageusername) checkusername = check_username_taken(pageusername)
@ -255,20 +239,20 @@ def user(pageusername):
if usersession: if usersession:
userCookie = get_session(usersession) userCookie = get_session(usersession)
user = get_user(userCookie["id"]) user = get_user(userCookie["id"])
return render_template("user.html", userdata=user, createddate=datetime.datetime.utcfromtimestamp(int(str(pageuser["created"]).split(".")[0])).strftime("%Y-%m-%d"), pageuser=pageuser) return await render_template("user.html", userdata=user, createddate=datetime.datetime.utcfromtimestamp(int(str(pageuser["created"]).split(".")[0])).strftime("%Y-%m-%d"), pageuser=pageuser)
else: else:
return render_template("user.html", createddate=datetime.datetime.utcfromtimestamp(int(str(pageuser["created"]).split(".")[0])).strftime("%Y-%m-%d"), pageuser=pageuser) return await render_template("user.html", createddate=datetime.datetime.utcfromtimestamp(int(str(pageuser["created"]).split(".")[0])).strftime("%Y-%m-%d"), pageuser=pageuser)
else: else:
return """<img src="https://http.cat/images/404.jpg">""", 404 return """<img src="https://http.cat/images/404.jpg">""", 404
@app.route("/api/page/<userid>", methods=("GET", "POST")) @app.route("/api/page/<userid>", methods=("GET", "POST"))
def apipageuser(userid): async def apipageuser(userid):
pageuser = get_user(userid) pageuser = get_user(userid)
addhtml = """ addhtml = """
<!DOCTYPE html><script> <!DOCTYPE html><script>
window.stop() window.stop()
</script> </script>
<base target="_blank"/> <head><meta http-equiv="Content-Security-Policy" default-src='none'; content="img-src cdn.discordapp.com cdn.discordapp.net media.tenor.com; style-src: 'self';" /></head>""" <base target="_blank"/> <head><meta http-equiv="Content-Security-Policy" default-src='none'; content="img-src cdn.discordapp.com cdn.discordapp.net media.tenor.com; media-src: fonts.gstatic.com fonts.googleapis.com; style-src: 'self';" /></head>"""
if not pageuser == "error": if not pageuser == "error":
return addhtml + pageuser["htmldescription"] return addhtml + pageuser["htmldescription"]
@ -276,7 +260,7 @@ def apipageuser(userid):
return """<img src="https://http.cat/images/404.jpg">""", 404 return """<img src="https://http.cat/images/404.jpg">""", 404
@app.route("/@<pageusername>/edit", methods=("GET", "POST")) @app.route("/@<pageusername>/edit", methods=("GET", "POST"))
def edituser(pageusername): async def edituser(pageusername):
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
checkusername = check_username_taken(pageusername) checkusername = check_username_taken(pageusername)
@ -288,7 +272,9 @@ def edituser(pageusername):
user = get_user(userCookie["id"]) user = get_user(userCookie["id"])
if pageuser["username"] == user["username"]: if pageuser["username"] == user["username"]:
if request.method == "POST": if request.method == "POST":
code = request.form["code"].replace("Content-Security-Policy", "").replace("<iframe>", "") requestData = await request.form
code = requestData["code"].replace("Content-Security-Policy", "").replace("<iframe>", "")
conn = get_db_connection() conn = get_db_connection()
conn.execute("UPDATE users SET htmldescription = ? WHERE id = ?", conn.execute("UPDATE users SET htmldescription = ? WHERE id = ?",
(code, user["id"])) (code, user["id"]))
@ -296,7 +282,7 @@ def edituser(pageusername):
conn.close() conn.close()
return redirect("/@" + user["username"]) return redirect("/@" + user["username"])
else: else:
return render_template("edituser.html", userdata=user, pageuser=pageuser) return await render_template("edituser.html", userdata=user, pageuser=pageuser)
else: else:
return """<img src="https://http.cat/images/403.jpg">""", 403 return """<img src="https://http.cat/images/403.jpg">""", 403
else: else:
@ -306,7 +292,7 @@ def edituser(pageusername):
@app.route("/api/frontpage", methods=("GET", "POST")) @app.route("/api/frontpage", methods=("GET", "POST"))
def apifrontpage(): async def apifrontpage():
conn = get_db_connection() conn = get_db_connection()
posts = conn.execute("SELECT * FROM posts ORDER BY created DESC;").fetchall() posts = conn.execute("SELECT * FROM posts ORDER BY created DESC;").fetchall()
conn.close() conn.close()
@ -346,7 +332,7 @@ def apifrontpage():
return result return result
@app.route("/api/userinfo", methods=("GET", "POST")) @app.route("/api/userinfo", methods=("GET", "POST"))
def apiuserinfo(): async def apiuserinfo():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
userCookie = get_session(usersession) userCookie = get_session(usersession)
@ -363,12 +349,11 @@ def apiuserinfo():
}, 403 }, 403
@limiter.limit("10/minute", override_defaults=False)
@app.route("/api/login", methods=("GET", "POST")) @app.route("/api/login", methods=("GET", "POST"))
def apilogin(): async def apilogin():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if request.method == "POST": if request.method == "POST":
data = request.get_json() data = await request.get_json()
username = data["username"] username = data["username"]
password = data["password"] password = data["password"]
@ -402,56 +387,56 @@ def apilogin():
}, 400 }, 400
@app.route("/apidocs", methods=("GET", "POST")) @app.route("/apidocs", methods=("GET", "POST"))
def apidocs(): async def apidocs():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
userCookie = get_session(usersession) userCookie = get_session(usersession)
user = get_user(userCookie["id"]) user = get_user(userCookie["id"])
return render_template("apidocs.html", userdata=user) return await render_template("apidocs.html", userdata=user)
else: else:
return render_template("apidocs.html") return await render_template("apidocs.html")
@app.route("/post", methods=("GET", "POST")) @app.route("/post", methods=("GET", "POST"))
def post(): async def post():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
if request.method == "POST": if request.method == "POST":
title = request.form["title"] formData = await request.form
formFiles = await request.files
title = formData["title"]
if title == "": if title == "":
flash("Text required :3") await flash("Text required :3")
return redirect(url_for("post")) return redirect(url_for("post"))
if len(title) > 300: if len(title) > 300:
flash("Too long title!") await flash("Too long title!")
return redirect(url_for("post")) return redirect(url_for("post"))
if "file" not in request.files: file = formFiles["file"]
flash("No file selected :3")
return redirect(url_for("post"))
file = request.files["file"] if (file):
if file.filename == "": if not allowed_file(file.filename):
flash("No file selected :3") await flash("File is not an image!")
return redirect(url_for("post")) return redirect(url_for("post"))
if not allowed_file(file.filename):
flash("File is not an image!")
return redirect(url_for("post"))
userCookie = get_session(usersession) userCookie = get_session(usersession)
user = get_user(userCookie["id"]) user = get_user(userCookie["id"])
if not user["banned"] == "0": if not user["banned"] == "0":
flash("Your account has been banned. You may no longer perform this action.") await flash("Your account has been suspended. You may no longer post")
return redirect(url_for("post")) return redirect(url_for("post"))
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
finalfilename = secrets.token_hex(32) + filename finalfilename = secrets.token_hex(32) + filename
file.save(os.path.join(UPLOAD_FOLDER, finalfilename)) await file.save(os.path.join(UPLOAD_FOLDER, finalfilename))
imgurl = "/cdn/" + finalfilename imgurl = "/cdn/" + finalfilename
if (not file):
imgurl = ""
conn = get_db_connection() conn = get_db_connection()
conn.execute("INSERT INTO posts (textstr, imageurl, creator, created) VALUES (?, ?, ?, ?)", conn.execute("INSERT INTO posts (textstr, imageurl, creator, created) VALUES (?, ?, ?, ?)",
(title, imgurl, userCookie["id"], str(time.time()))) (title, imgurl, userCookie["id"], str(time.time())))
@ -462,20 +447,19 @@ def post():
else: else:
userCookie = get_session(usersession) userCookie = get_session(usersession)
user = get_user(userCookie["id"]) user = get_user(userCookie["id"])
return render_template("post.html", userdata=user) return await render_template("post.html", userdata=user)
else: else:
flash("A burgercat account is required to post :3") flash("A burgercat account is required to post :3")
return redirect(url_for("login")) return redirect(url_for("login"))
@app.route("/api/comment", methods=("GET", "POST")) @app.route("/api/comment", methods=("GET", "POST"))
@limiter.limit("1/second", override_defaults=False) async def comment():
def comment():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
if request.method == "POST": if request.method == "POST":
data = request.get_json() data = await request.get_json()
uid = data["id"] uid = data["id"]
title = data["title"] title = data["title"]
@ -510,7 +494,7 @@ def comment():
}, 401 }, 401
@app.route("/api/post/<post_id>/comments", methods=("GET", "POST")) @app.route("/api/post/<post_id>/comments", methods=("GET", "POST"))
def apicomments(post_id): async def apicomments(post_id):
postthing = get_comments(int(post_id)) postthing = get_comments(int(post_id))
if not postthing == "error": if not postthing == "error":
@ -528,61 +512,62 @@ def apicomments(post_id):
return comments return comments
@app.route("/cdn/<filename>", methods=("GET", "POST")) @app.route("/cdn/<filename>", methods=("GET", "POST"))
@limiter.exempt async def cdn(filename):
def cdn(filename):
if os.path.exists(os.path.join(UPLOAD_FOLDER, filename)): if os.path.exists(os.path.join(UPLOAD_FOLDER, filename)):
return send_from_directory(UPLOAD_FOLDER, filename) return await send_from_directory(UPLOAD_FOLDER, filename)
else: else:
return "file doesn't exist!!" return "file doesn't exist!!"
@app.route("/signup", methods=("GET", "POST")) @app.route("/signup", methods=("GET", "POST"))
@limiter.limit("5/minute", override_defaults=False) async def signup():
def signup():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
return redirect(url_for("main")) return redirect(url_for("main"))
if request.method == "POST": if request.method == "POST":
if not check_username_taken(request.form["username"]) == "error": requestData = await request.form
flash("Username already taken :3")
if not check_username_taken(requestData["username"]) == "error":
await flash("Username already taken :3")
return redirect(url_for("signup")) return redirect(url_for("signup"))
if not request.form["username"].isalnum(): if not requestData["username"].isalnum():
flash("Username must be alphanumeric :3") await flash("Username must be alphanumeric :3")
return redirect(url_for("signup")) return redirect(url_for("signup"))
if not len(request.form["password"]) > int(PASSWORD_REQUIREMENT): if not len(requestData["password"]) > int(PASSWORD_REQUIREMENT):
flash("Password must contain at least " + PASSWORD_REQUIREMENT + " characters") await flash("Password must contain at least " + PASSWORD_REQUIREMENT + " characters")
return redirect(url_for("signup")) return redirect(url_for("signup"))
hashedpassword = generate_password_hash(request.form["password"]) hashedpassword = generate_password_hash(requestData["password"])
conn = get_db_connection() conn = get_db_connection()
conn.execute("INSERT INTO users (username, password, created, htmldescription) VALUES (?, ?, ?, ?)", conn.execute("INSERT INTO users (username, password, created, htmldescription) VALUES (?, ?, ?, ?)",
(request.form["username"], hashedpassword, str(time.time()), "")) (requestData["username"], hashedpassword, str(time.time()), ""))
conn.commit() conn.commit()
conn.close() conn.close()
return redirect(url_for("login")) return redirect(url_for("login"))
else: else:
return render_template("signup.html") return await render_template("signup.html")
@app.route("/login", methods=("GET", "POST")) @app.route("/login", methods=("GET", "POST"))
@limiter.limit("10/minute", override_defaults=False) async def login():
def login():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
redirect(url_for("main")) redirect(url_for("main"))
if request.method == "POST": if request.method == "POST":
userID = check_username_taken(request.form["username"]) requestData = await request.form
userID = check_username_taken(requestData["username"])
user = get_user(userID) user = get_user(userID)
if user == "error": if user == "error":
flash("Wrong username or password :3") flash("Wrong username or password :3")
return redirect(url_for("login")) return redirect(url_for("login"))
if not check_password_hash(user["password"], (request.form["password"])): if not check_password_hash(user["password"], (requestData["password"])):
flash("Wrong username or password :3") flash("Wrong username or password :3")
return redirect(url_for("login")) return redirect(url_for("login"))
@ -594,32 +579,37 @@ def login():
conn.commit() conn.commit()
conn.close() conn.close()
resp = make_response(redirect("/")) response = Response("""<script>window.location.href = "/";</script>""")
resp.set_cookie("session_DO_NOT_SHARE", randomCharacters, expires=datetime.datetime.now() + datetime.timedelta(days=float(180))) response.set_cookie("session_DO_NOT_SHARE", randomCharacters)
return response
#resp = await make_response(redirect("/"))
#resp.set_cookie("session_DO_NOT_SHARE", randomCharacters, expires=datetime.datetime.now() + datetime.timedelta(days=float(180)))
#return redirect("/")
return resp
else: else:
return render_template("login.html") return await render_template("login.html")
@app.route("/settings", methods=("GET", "POST")) @app.route("/settings", methods=("GET", "POST"))
def settings(): async def settings():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
userCookie = get_session(usersession) userCookie = get_session(usersession)
user = get_user(userCookie["id"]) user = get_user(userCookie["id"])
return render_template("settings.html", userdata=user, createddate=datetime.datetime.utcfromtimestamp(int(str(user["created"]).split(".")[0])).strftime("%Y-%m-%d %H:%m:%S")) return await render_template("settings.html", userdata=user, createddate=datetime.datetime.utcfromtimestamp(int(str(user["created"]).split(".")[0])).strftime("%Y-%m-%d %H:%m:%S"))
else: else:
return redirect("/") return redirect("/")
@app.route("/api/delete", methods=("GET", "POST")) @app.route("/api/delete", methods=("GET", "POST"))
def delete(): async def delete():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if request.method == "POST": if request.method == "POST":
data = request.get_json() data = await request.get_json()
postid = int(data["id"]) postid = int(data["id"])
post = get_post(postid) post = get_post(postid)
@ -645,7 +635,7 @@ def delete():
}, 400 }, 400
@app.route("/listusers", methods=("GET", "POST")) @app.route("/listusers", methods=("GET", "POST"))
def listusers(): async def listusers():
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
if usersession: if usersession:
userCookie = get_session(usersession) userCookie = get_session(usersession)
@ -667,7 +657,7 @@ def listusers():
@app.route("/settings/logout", methods=("GET", "POST")) @app.route("/settings/logout", methods=("GET", "POST"))
def logout(): async def logout():
resp = redirect(url_for("main")) resp = redirect(url_for("main"))
session = request.cookies.get("session_DO_NOT_SHARE") session = request.cookies.get("session_DO_NOT_SHARE")
resp.delete_cookie("session_DO_NOT_SHARE") resp.delete_cookie("session_DO_NOT_SHARE")
@ -675,34 +665,32 @@ def logout():
return resp return resp
@app.errorhandler(500) @app.errorhandler(500)
def page_not_found(e): async def page_not_found(e):
return """<img src="https://http.cat/images/500.jpg">""", 500 return """<img src="https://http.cat/images/500.jpg">""", 500
@app.errorhandler(400) @app.errorhandler(400)
def page_not_found(e): async def page_not_found(e):
return """<img src="https://http.cat/images/400.jpg">""", 400 return """<img src="https://http.cat/images/400.jpg">""", 400
@app.errorhandler(429) @app.errorhandler(429)
def page_not_found(e): async def page_not_found(e):
return """<img src="https://http.cat/images/429.jpg">""", 429 return """<img src="https://http.cat/images/429.jpg">""", 429
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): async def page_not_found(e):
return """<img src="https://http.cat/images/404.jpg">""", 404 return """<img src="https://http.cat/images/404.jpg">""", 404
@app.errorhandler(413) @app.errorhandler(413)
def page_not_found(e): async def page_not_found(e):
return "Images can't be larger than " + str(UPLOAD_LIMIT) + "MB", 413 return "Images can't be larger than " + str(UPLOAD_LIMIT) + "MB", 413
if __name__ == "__main__": if __name__ == "__main__":
print("[INFO] Server started") print("[INFO] Server started")
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock: hypercornconfig = Config()
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) hypercornconfig.bind = ("0.0.0.0" + ":" + PORT)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) asyncio.run(serve(app, hypercornconfig))
sock.bind(('', int(PORT)))
serve(app, sockets=[sock], threads=9999)
print("[INFO] Server stopped") print("[INFO] Server stopped")

View File

@ -1,7 +1,4 @@
Flask quart
Flask-Limiter
werkzeug werkzeug
waitress hypercorn
requests requests
Flask-SSE
APScheduler

View File

@ -1,4 +1,5 @@
@import url("https://fonts.googleapis.com/css2?family=Inter&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Inter&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap");
body { body {
margin: 0; margin: 0;
@ -10,13 +11,18 @@ body {
margin: 0; margin: 0;
padding: 10px; padding: 10px;
border: solid; border: solid;
border-color: grey; border-color: rgb(195, 195, 195);
border-width: 0; border-width: 0;
border-bottom-width: 1px; border-bottom-width: 1px;
background-color: white; background-color: white;
width: 100%; width: 100%;
position: fixed; position: fixed;
z-index: 2; z-index: 2;
font-family: "Space Grotesk", sans-serif;
font-optical-sizing: auto;
}
.navbar h1 {
font-weight: 600;
} }
.bottom { .bottom {
@ -33,11 +39,16 @@ body {
text-decoration: none; text-decoration: none;
} }
.navbar a {
padding-left: 5px;
padding-right: 5px;
}
.navbar .selected { .navbar .selected {
border: solid; border: solid;
border-color: #f1b739; border-color: #f1b739;
border-width: 0; border-width: 0;
border-bottom-width: 2px; border-bottom-width: 3px;
} }
.postDiv { .postDiv {
@ -80,6 +91,11 @@ body {
margin-right: 20%; margin-right: 20%;
} }
.accountform h1 {
font-family: "Space Grotesk", sans-serif;
font-optical-sizing: auto;
}
.accountform input { .accountform input {
padding: 7px; padding: 7px;
border: solid; border: solid;
@ -128,12 +144,17 @@ body {
.post { .post {
margin: 10px; margin: 10px;
padding: 5px; padding: 10px;
margin-bottom: 10px; margin-bottom: 10px;
border-radius: 10px; border-radius: 10px;
background-color: rgb(250, 250, 250); background-color: rgb(250, 250, 250);
} }
.post h2, .post h3 {
font-family: "Space Grotesk", sans-serif;
font-optical-sizing: auto;
}
.post img { .post img {
min-height: 200px; min-height: 200px;
max-height: 300px; max-height: 300px;

File diff suppressed because one or more lines are too long

View File

@ -76,25 +76,6 @@ function selectChannel(id) {
updateMessages(id) updateMessages(id)
} }
function selectChannel(id) {
channelID = id
let selectedButton = channelDiv.querySelector(".selected");
if (selectedButton) {
selectedButton.classList.remove("selected");
}
let channelButton = document.getElementById("channelButton" + id)
if (channelButton) {
channelButton.classList.add("selected")
}
else {
console.log("channelButton not found")
}
updateMessages(id)
}
async function updateRooms() { async function updateRooms() {
let response = await fetch("/api/chat/listrooms"); let response = await fetch("/api/chat/listrooms");
let rooms = await response.json() let rooms = await response.json()
@ -122,32 +103,34 @@ async function sendMessage(content, id) {
}) })
} }
let updateInterval = 4100
messageBox.addEventListener("keyup", function onEvent(event) { messageBox.addEventListener("keyup", function onEvent(event) {
if (event.key === "Enter") { if (event.key === "Enter") {
if (!messageBox.value == "") { if (!messageBox.value == "") {
if (messageBox.value.length < 140) { if (messageBox.value.length < 140) {
sendMessage(messageBox.value, channelID) sendMessage(messageBox.value, channelID)
messageBox.value = "" messageBox.value = ""
updateInterval = 1300
} }
updateMessages(channelID)
} }
} }
}) })
let messageStream = new EventSource("/stream") function messageTimer(){
window.addEventListener("load", function () {
updateRooms()
updateMessages(channelID) updateMessages(channelID)
if (updateInterval < 6000) {
updateInterval = updateInterval + 100
}
messageStream.addEventListener("publish", function (event) { console.log(updateInterval)
results = JSON.parse(event.data) setTimeout(messageTimer, updateInterval);
}
if (Number(results["message"]["roomid"]) == channelID) { messageTimer();
addMessage(results["message"]["content"], results["message"]["created"], results["message"]["creator"], results["message"]["roomid"])
}
}) updateRooms()
}) updateMessages()
window.addEventListener("beforeunload", function () {
messageStream.close()
})

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>burgercat</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" type="text/css" href="/static/css/style.css" />
</head>
<body>
<div class="post">
<h2>burgercat is permanently shutting down on July 21th.</h2>
</div>
<div class="post">
<p>After 19 days of service, burgercat is permanently shutting down on July 21th 2023. 😔<br><br>
After July 21th, you will no longer be able to access burgercat. Please back up anything you want to keep before July 21th.</p>
<p>The Codeberg repository will be archived. Third-party instances will not be affected.</p>
<p><b>Thank you for using burgercat</b>, and thank <a href="https://codeberg.org/ffqq">@ffqq</a>, <a href="https://codeberg.org/carsand101">@carsand101</a>, <a href="https://codeberg.org/Mollomm1">@Mollomm1</a>, <a href="https://codeberg.org/TestingPlant">@TestingPlant</a>, <a href="https://codeberg.org/sewn">@sewn</a>, and <a href="/@Anonymous">@Anonymous</a> for contributing to the project.</p>
</div>
</body>
</html>

View File

@ -13,8 +13,9 @@
<div class="navbar"> <div class="navbar">
<h1>burgercat</h1> <h1>burgercat</h1>
<a href="/">home</a> <a href="/">home</a>
<a href="/chat">chat</a>
<a href="/post">post</a> <a href="/post">post</a>
<a class="selected" href="/apidocs">API</a> <a class="selected" href="/apidocs">api</a>
{% if userdata %} {% if userdata %}
<a href="/settings/logout" class="right r">log out</a> <a href="/settings/logout" class="right r">log out</a>
<a href="/settings" class="right">{{ userdata.username }}</a> <a href="/settings" class="right">{{ userdata.username }}</a>
@ -32,7 +33,7 @@
<div class="postDiv"> <div class="postDiv">
<div class="post"> <div class="post">
<h2>burgercat API documentation</h2> <h2>burgercat API documentation</h2>
for API things that require authentication, you will need to set the <code>session_DO_NOT_SHARE</code> cookie. the key might expire after 180 days.<br><br> for API endpoints that require authentication, you'll need to set the <code>session_DO_NOT_SHARE</code> cookie. key might expire after 180 days.<br><br>
GET <code>/api/frontpage</code> - returns frontpage<br><br> GET <code>/api/frontpage</code> - returns frontpage<br><br>
POST <code>/post</code> - post ctas - authentication required<br> POST <code>/post</code> - post ctas - authentication required<br>
@ -43,9 +44,9 @@
POST <code>/api/delete</code> - remove posts - authentication required<br> POST <code>/api/delete</code> - remove posts - authentication required<br>
<code>id</code>, being the id of the post. you must be the owner of the post to delete.<br><br> <code>id</code>, being the id of the post. you must be the owner of the post to delete.<br><br>
POST <code>/api/login</code> - get authentication key<br> POST <code>/api/login</code> - get authentication key<br>
<code>username</code>, being the username and <code>password</code>, being the password. Returns authentication key.<br><br> <code>username</code>, being the username and <code>password</code>, being the password.<br><br>
GET <code>/api/userinfo</code> - authentication required - Returns user info, username, ID, and account creation date.<br><br> GET <code>/api/userinfo</code> - authentication required - Returns user info, username, ID, and account creation date.<br><br>
GET <code>/api/post/1/comments</code> - Returns comments of post GET <code>/api/post/1/comments</code> - Returns comments of post. Replace 1 with the post ID.
</div> </div>
</div> </div>
</body> </body>

View File

@ -36,7 +36,6 @@
<p class="warning">Your account has been banned. Reason: "{{ userdata.banned }}". <a href="/settings/logout">Log out</a></p> <p class="warning">Your account has been banned. Reason: "{{ userdata.banned }}". <a href="/settings/logout">Log out</a></p>
{% endif %} {% endif %}
{% endif %} {% endif %}
<p class="warning">⚠️ burgercat will permanently shut down on July 21th. ⚠️ <a href="/static/shutdown.html">Read more</a></p>
{% for post in posts %} {% for post in posts %}
<div class="post" id="post"> <div class="post" id="post">
<p><a href='/@{{ getUser(post["creator"])["username"] }}' class="username usernamelink">{{ getUser(post["creator"])["username"] }}</a></p> <p><a href='/@{{ getUser(post["creator"])["username"] }}' class="username usernamelink">{{ getUser(post["creator"])["username"] }}</a></p>
@ -44,7 +43,9 @@
{% if userdata %} {% if userdata %}
<p class="hidden" id="usernameElement">{{ userdata.username }}</p> <p class="hidden" id="usernameElement">{{ userdata.username }}</p>
{% endif %} {% endif %}
{% if post["imageurl"] != "" %}
<img loading="lazy" src='{{ post["imageurl"] }}'> <img loading="lazy" src='{{ post["imageurl"] }}'>
{% endif %}
<p class="text">{{ post["textstr"] }}</p> <p class="text">{{ post["textstr"] }}</p>
{% if getComments(post["id"]) | length > 0 %} {% if getComments(post["id"]) | length > 0 %}
<div id="commentBurgerDiv" class="commentsdiv"> <div id="commentBurgerDiv" class="commentsdiv">
@ -114,12 +115,6 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="bottom">
<span>
<a href="https://codeberg.org/burger-software/burgercat">burgercat</a>
<a href="https://codeberg.org/burger-software/burgercat/commit/{{ full_hash }}" style="float: right;">commit hash: {{ short_hash }}</a>
</span>
</div>
<script src="/static/js/main.js"></script> <script src="/static/js/main.js"></script>
</body> </body>

View File

@ -34,28 +34,28 @@
{% endif %} {% endif %}
{% endwith %} {% endwith %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<h2>Post</h2> <h2>New post</h2>
<p>Image</p> <p>Image</p>
<input type="file" name="file" required> <input type="file" name="file">
<br> <br>
<p>Text</p> <p>Text (required)</p>
<input name="title" type="text" placeholder="Text" required> <input name="title" type="text" placeholder="Type something here.." required>
<br><br> <br><br>
<input class="submit" type="submit" value="Post"> <input class="submit" type="submit" value="Post">
</form> </form>
</div> </div>
<div class="post"> <div class="post">
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<h2>Rules</h2> <h2>Posting Guidelines</h2>
<p>keep burgercat amazing: follow the posting guidelines</p>
<ul> <ul>
<li>Please do not spam</li> <li>Spam content isn't allowed, this includes promoting services/products.</li>
<li>Treat everyone with respect, respect each others opinions</li> <li>Treat everyone with respect, please respect others opinions.</li>
<li>Posting NSFW content is strictly prohibited, doing so will get you an instant ban</li> <li>Posting NSFW content is strictly prohibited. This includes suggestive content.</li>
<li>Discussions regarding politics and related controversial topics are disallowed</li> <li>We have zero-tolerance for racism, homophobia, transphobia etc.</li>
<li>Advertising is not allowed</li> <li>Avoid politics and rage-bait.</li>
<li>Do not post links</li>
<li>Do not create alt-accounts to evade bans</li>
</ul> </ul>
<p>You might be moderated for things not listed here, use common sense.</p>
</form> </form>
</div> </div>
</div> </div>

View File

@ -26,13 +26,14 @@
<div class="postDiv"> <div class="postDiv">
<div class="post"> <div class="post">
<h3>Account</h3> <h2>Your account</h2>
<p>Account Type: {% if userdata.administrator == 1 %}Administrator{% else %}Normal{% endif %}<br> <a href="/@{{ userdata.username }}">View my public profile</a><br>
<h3>Information</h3>
<p>Account Type: {% if userdata.administrator == 1 %}Administrator{% else %}Regular{% endif %}<br>
Username: {{ userdata.username }}<br> Username: {{ userdata.username }}<br>
User ID: {{ userdata.id }}<br> User ID: {{ userdata.id }}<br>
Created on: {{ createddate }}</p> Created on: {{ createddate }}</p>
<br> <h3>Actions</h3>
<a href="/@{{ userdata.username }}">View my public profile</a><br>
<a href="/settings/logout">Log out</a> <a href="/settings/logout">Log out</a>
</div> </div>
</div> </div>