From adf6ad863c5d3f0d0dc43a141ad2055e2cd58512 Mon Sep 17 00:00:00 2001 From: maaa Date: Tue, 11 Jul 2023 21:41:57 +0200 Subject: [PATCH 01/31] add chat --- README.md | 3 ++ main | 121 +++++++++++++++++++++++++++++++++++++++++-- newchatroom | 18 +++++++ requirements.txt | 3 +- schema.sql | 17 ++++++ static/css/style.css | 96 ++++++++++++++++++++++++++++++++++ static/js/chat.js | 90 ++++++++++++++++++++++++++++++++ templates/chat.html | 56 ++++++++++++++++++++ templates/main.html | 1 + templates/post.html | 1 + 10 files changed, 401 insertions(+), 5 deletions(-) create mode 100644 newchatroom create mode 100644 static/js/chat.js create mode 100644 templates/chat.html diff --git a/README.md b/README.md index 4c66f85..d13ed3e 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,6 @@ python main ### zero downtime restarts: - launch new burgercat server - close previous server + +### contribution guidelines: +- please check that your PR does not break anything \ No newline at end of file diff --git a/main b/main index a5c1997..4dcc714 100644 --- a/main +++ b/main @@ -82,6 +82,16 @@ def get_comments(id): return post +def get_messages(chatroomid): + conn = get_db_connection() + post = conn.execute("SELECT * FROM chatmessages WHERE chatroom_id = ? ORDER BY created DESC;", + (chatroomid,)).fetchall() + conn.close() + if post is None: + return "error" + return post + + app.jinja_env.globals.update(getComments=get_comments) def get_post(id): @@ -138,6 +148,109 @@ def main(): else: return render_template("main.html", posts=posts) +@app.route("/chat", methods=("GET", "POST")) +def chat(): + usersession = request.cookies.get("session_DO_NOT_SHARE") + if usersession: + userCookie = get_session(usersession) + user = get_user(userCookie["id"]) + return render_template("chat.html", userdata=user) + else: + return render_template("chat.html") + +@app.route("/api/chat/listrooms") +def chatlistrooms(): + conn = get_db_connection() + rooms = conn.execute("SELECT * FROM chatrooms ORDER BY roomname ASC;").fetchall() + conn.close() + + template = [] + + for room in rooms: + roomtemplate = { + "id": room["id"], + "name": room["roomname"] + } + template.append(roomtemplate) + + return(template), 200 + +@app.route("/api/chat/getmessages/") +def chatget(roomid): + messages = get_messages(roomid) + + template = [] + + for message in messages: + creatorid = message["creator"] + + creatortemplate = { + "id": message["creator"], + "username": get_user(creatorid)["username"] + } + + messagetemplate = { + "id": message["id"], + "content": message["content"], + "creator": creatortemplate, + "created": message["created"] + } + template.append(messagetemplate) + + return(template), 200 + +burgerMessageUpdate = True +burgerMessageCache = "" + +@app.route("/api/chat/send/", methods=("GET", "POST")) +def chatsend(roomid): + usersession = request.cookies.get("session_DO_NOT_SHARE") + if usersession: + if request.method == "POST": + + data = request.get_json() + content = data["content"] + + print(content) + print(roomid) + + userCookie = get_session(usersession) + user = get_user(userCookie["id"]) + + if not user["banned"] == "0": + return { + "error": "banned" + }, 403 + + conn = get_db_connection() + conn.execute("INSERT INTO chatmessages (content, chatroom_id, creator, created) VALUES (?, ?, ?, ?)", + (content, roomid, userCookie["id"], str(time.time()))) + conn.commit() + conn.close() + + global burgerMessageCache + global burgerMessageUpdate + burgerMessageUpdate = True + burgerMessageCache = user["username"] + ": " + content + + return "success", 200 + + +@sock.route("/api/chat/listen") +def chatlisten(ws): + global burgerMessageCache + global burgerMessageUpdate + + while burgerMessageUpdate == True: + ws.send(burgerMessageCache) + burgerMessageUpdate = False + +@app.route("/erm", methods=("GET", "POST")) +def erm(): + print(burgerMessageCache) + print(burgerMessageUpdate) + + return "fart" @app.route("/@", methods=("GET", "POST")) def user(pageusername): @@ -290,7 +403,7 @@ def apilogin(): return { "key": randomCharacters - }, 100 + }, 200 else: return { "error": "https://http.cat/images/400.jpg" @@ -346,7 +459,7 @@ def apipost(): conn.commit() conn.close() - return "success", 100 + return "success", 200 @app.route("/apidocs", methods=("GET", "POST")) def apidocs(): @@ -445,7 +558,7 @@ def comment(): conn.commit() conn.close() - return "success", 100 + return "success", 200 else: return { @@ -581,7 +694,7 @@ def delete(): conn.execute("DELETE FROM posts WHERE id = ?", (postid,)) conn.commit() conn.close() - return "success", 100 + return "success", 200 else: return { "error": "https://http.cat/images/403.jpg" diff --git a/newchatroom b/newchatroom new file mode 100644 index 0000000..b446d1f --- /dev/null +++ b/newchatroom @@ -0,0 +1,18 @@ +#!/usr/bin/python3 +import sqlite3 +import time +import os + +chatroomname = input("Insert name: ") + +def get_db_connection(): + conn = sqlite3.connect("database.db") + conn.row_factory = sqlite3.Row + return conn + +conn = get_db_connection() +conn.execute("INSERT INTO chatrooms (roomname, creator, created) VALUES (?, ?, ?)", + (chatroomname, 1, str(time.time()))) +conn.commit() +conn.close() +print("Success!") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c9ec32e..79aec11 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flask Flask-Limiter werkzeug -waitress \ No newline at end of file +waitress +requests \ No newline at end of file diff --git a/schema.sql b/schema.sql index f3409d8..d5b00b2 100644 --- a/schema.sql +++ b/schema.sql @@ -2,6 +2,8 @@ DROP TABLE IF EXISTS users; DROP TABLE IF EXISTS posts; DROP TABLE IF EXISTS comments; DROP TABLE IF EXISTS sessions; +DROP TABLE IF EXISTS chatrooms; +DROP TABLE IF EXISTS chatmessages; CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -29,6 +31,21 @@ CREATE TABLE comments ( textstr TEXT NOT NULL ); +CREATE TABLE chatrooms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created TEXT NOT NULL, + creator TEXT NOT NULL, + roomname TEXT NOT NULL +); + +CREATE TABLE chatmessages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chatroom_id INTEGER NOT NULL, + created TEXT NOT NULL, + creator TEXT NOT NULL, + content TEXT NOT NULL +); + CREATE TABLE sessions ( session TEXT PRIMARY KEY NOT NULL, id INTEGER NOT NULL diff --git a/static/css/style.css b/static/css/style.css index e8d9cd8..7cbfcd9 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -16,20 +16,25 @@ body { background-color: white; width: 100%; position: fixed; + z-index: 2; } + .navbar .selected { border: solid; border-color: #f1b739; border-width: 0; border-bottom-width: 2px; } + .postDiv { padding-top: 120px; } + .profileDiv { padding-left: 10px; padding-right: 10px; } + .profileIFrame { border: solid; border-width: 0; @@ -38,6 +43,7 @@ body { width: 100%; height: calc(100vh - 295px); } + .profileDiv .badgeDiv p { background-color: black; border-radius: 99px; @@ -47,15 +53,18 @@ body { padding-right: 10px; display: inline; } + .editThing .htmlBox { width: calc(100% - 10px); height: 300px; text-align: top; } + .accountform { margin-left: 20%; margin-right: 20%; } + .accountform input { padding: 7px; border: solid; @@ -65,6 +74,7 @@ body { font-size: 16px; border-radius: 8px; } + .accountform .flash { padding: 7px; border: solid; @@ -74,6 +84,7 @@ body { font-size: 16px; border-radius: 8px; } + .accountform button { padding: 7px; border: solid; @@ -124,6 +135,7 @@ body { font-size: 16px; font-family: "Inter", sans-serif; } + .post button:hover { border-color: #f1b739; } @@ -138,11 +150,13 @@ body { margin-top: 5px; font-family: "Inter", sans-serif; } + .post hr { color: rgb(255, 255, 255); background-color: rgb(255, 255, 255); border-color: rgb(255, 255, 255); } + .post .commentsdiv { background-color: #f5f5f5; padding: 5px; @@ -163,6 +177,88 @@ body { text-decoration: none; } +.chatDiv { + width: 292vh; + margin-top: 2px; + height: calc(100vh - 121px); +} + +.channelDiv { + position: absolute; + width: 220px; + height: calc(100%); + left: 0; + background-color: rgb(245, 245, 245); +} + +.channelDiv .statusMessage { + position: absolute; + left: 10px; + bottom: 122.5px; +} + +.channelDiv button { + width: calc(100% - 20px); + margin: 10px; + margin-bottom: 0; + padding-left: 10px; + background-color: white; + border: none; + border-radius: 8px; + color: black; + font-size: 16px; + text-align: left; + height: 40px; +} + +.channelDiv .selected { + border: solid; + border-width: 1px; + border-color: #f1b739; +} + +.channelDiv button:hover { + background-color: rgb(249, 249, 249); + transition: background-color ease-in-out 100ms +} + +.messageDiv { + position: absolute; + width: calc(100% - 220px); + height: calc(100%); + padding-bottom: 50px; + right: 0; + margin-top: -180px; + display: flex; + flex-direction: column-reverse; +} + +.messageDiv p { + font-size: 16px; + margin-left: 10px; + line-height: 8px; + align-self: flex-start; +} + +.messageBar { + position: absolute; + width: calc(100% - 220px); + height: 50px; + right: 0; + bottom: 0; + background-color: rgb(228, 228, 228); +} + +.messageBar input { + width: calc(100% - 20px); + height: calc(100% - 15px); + border: none; + border-radius: 99px; + padding-left: 10px; + font-size: 15px; + margin: 5px; +} + .warning { width: 100%; background-color: #f1b739; diff --git a/static/js/chat.js b/static/js/chat.js new file mode 100644 index 0000000..a2eccdb --- /dev/null +++ b/static/js/chat.js @@ -0,0 +1,90 @@ +let channelDiv = document.getElementById("channelDiv") +let messageDiv = document.getElementById("messageDiv") +let messageBox = document.getElementById("messageBox") +let statusMessage = document.getElementById("statusMessage") + +let channelID = 0 + +async function updateMessages(id) { + try { + let response = await fetch("/api/chat/getmessages/" + id); + let messages = await response.json() + statusMessage.innerText = "" + document.querySelectorAll(".messageParagraph").forEach(el => el.remove()); + for (let i in messages) { + let messageParagraph = document.createElement("p") + messageParagraph.appendChild(document.createTextNode(messages[i]["creator"]["username"] + ": " + messages[i]["content"])) + messageParagraph.classList.add("messageParagraph") + messageParagraph.id = "messageParagraph" + messages[i]["id"] + messageDiv.append(messageParagraph) + } + } + catch { + statusMessage.innerText = "Not connected" + } +} + +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() { + let response = await fetch("/api/chat/listrooms"); + let rooms = await response.json() + for (let i in rooms) { + let channelButton = document.createElement("button") + channelButton.appendChild(document.createTextNode(rooms[i]["name"])) + channelButton.id = "channelButton" + rooms[i]["id"] + channelButton.onclick = function () { selectChannel(rooms[i]["id"]) } + + channelDiv.append(channelButton) + } + + selectChannel(1) +} + +async function sendMessage(content, id) { + fetch("/api/chat/send/" + String(id), { + method: "POST", + body: JSON.stringify({ + content: content + }), + headers: { + "Content-Type": "application/json" + } + }) +} + +messageBox.addEventListener("keyup", function onEvent(event) { + if (event.key === "Enter") { + sendMessage(messageBox.value, channelID) + updateMessages(channelID) + messageBox.value = "" + } +}) + +function update() { + updateMessages(channelID) + + setTimeout(update, 1500); +} + +window.addEventListener("load", function () { + updateRooms() + update() +}) \ No newline at end of file diff --git a/templates/chat.html b/templates/chat.html new file mode 100644 index 0000000..5355c1a --- /dev/null +++ b/templates/chat.html @@ -0,0 +1,56 @@ + + + + + + + burgercat + + + + + + + + + +
+ {% if userdata %} + {% if userdata.banned == "0" %} + {% else %} +

Your account has been banned. Reason: "{{ userdata.banned }}". Log out

+ {% endif %} + {% endif %} + +
+
+

Connected

+
+
+
+
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/templates/main.html b/templates/main.html index 7bd5c87..d09b766 100644 --- a/templates/main.html +++ b/templates/main.html @@ -13,6 +13,7 @@ - + -- 2.39.2 From df75e02efc359979d3bcb80b8c5ae11949f75be1 Mon Sep 17 00:00:00 2001 From: ffqq Date: Thu, 13 Jul 2023 02:50:16 +0000 Subject: [PATCH 20/31] fix: wrap links inside of for convenience --- static/js/chat.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/js/chat.js b/static/js/chat.js index 52d97c1..92116c2 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -20,7 +20,11 @@ async function updateMessages(id) { const timeParagraph = document.createElement("p"); const { creator, content, id, created } = message; - messageParagraph.appendChild(document.createTextNode(`${creator.username}: ${content}`)); + // Check if the message content contains any links that are not image links + const linkRegex = /(https?:\/\/[^\s]+(?$1"); + + messageParagraph.innerHTML = `${creator.username}: ${messageContent}`; messageParagraph.classList.add("messageParagraph"); messageParagraph.id = `messageParagraph${id}`; messageParagraph.appendChild(timeParagraph); -- 2.39.2 From 69d60208656c95d509a05d3b95b732adf810429a Mon Sep 17 00:00:00 2001 From: ffqq Date: Thu, 13 Jul 2023 03:05:33 +0000 Subject: [PATCH 21/31] fix: made commit hash consistent with the running server --- main | 19 ++++--------------- templates/main.html | 2 +- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/main b/main index 0114a2e..57a5084 100644 --- a/main +++ b/main @@ -127,17 +127,8 @@ def get_session(id): return "error" return post -def get_current_commit(output_format="full"): - if output_format == "short": - length = 8 - else: - length = 40 - - try: - output = subprocess.check_output(["git", "rev-parse", f"--short={length}", "HEAD"]).decode().strip() - return output - except subprocess.CalledProcessError: - return "Error fetching git commit" +full_hash = subprocess.check_output(["git", "rev-parse", "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"} @@ -151,16 +142,14 @@ def main(): usersession = request.cookies.get("session_DO_NOT_SHARE") conn = get_db_connection() posts = conn.execute("SELECT * FROM posts ORDER BY created DESC;").fetchall() - commit_hash_long = get_current_commit() - commit_hash_short = get_current_commit(output_format="short") conn.close() if usersession: userCookie = get_session(usersession) user = get_user(userCookie["id"]) - return render_template("main.html", userdata=user, posts=posts, commit_hash_long=commit_hash_long, commit_hash_short=commit_hash_short) + return render_template("main.html", userdata=user, posts=posts, full_hash=full_hash, short_hash=short_hash) else: - return render_template("main.html", posts=posts, commit_hash_long=commit_hash_long, commit_hash_short=commit_hash_short) + return render_template("main.html", posts=posts, full_hash=full_hash, short_hash=short_hash) @app.route("/chat", methods=("GET", "POST")) def chat(): diff --git a/templates/main.html b/templates/main.html index f75d0c1..2a3932f 100644 --- a/templates/main.html +++ b/templates/main.html @@ -116,7 +116,7 @@ -- 2.39.2 From 7b1e636187ad8c3418a831f178e6945ee00ae299 Mon Sep 17 00:00:00 2001 From: ffqq Date: Thu, 13 Jul 2023 03:22:47 +0000 Subject: [PATCH 22/31] feat: hide image links from messages and improve overall handling of links --- static/js/chat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/chat.js b/static/js/chat.js index 92116c2..1c7194e 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -20,8 +20,8 @@ async function updateMessages(id) { const timeParagraph = document.createElement("p"); const { creator, content, id, created } = message; - // Check if the message content contains any links that are not image links - const linkRegex = /(https?:\/\/[^\s]+(?$1"); messageParagraph.innerHTML = `${creator.username}: ${messageContent}`; -- 2.39.2 From 40fcc16cc5178b0cc81392dc4c4c6e6f780d7c16 Mon Sep 17 00:00:00 2001 From: ffqq Date: Thu, 13 Jul 2023 05:27:14 +0000 Subject: [PATCH 23/31] fix: inconsistency between dark mode and light mode --- static/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/style.css b/static/css/style.css index 7df4fbe..7ac1133 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -351,7 +351,7 @@ body { } .bottom a { - color: var(--gray-800) + color: var(--gray-700) } .navbar a, a { -- 2.39.2 From cbbdf37e57c9b5e5b19c0e08f048ab512822e526 Mon Sep 17 00:00:00 2001 From: maaa Date: Thu, 13 Jul 2023 16:51:21 +0200 Subject: [PATCH 24/31] update README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d13ed3e..206be24 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,6 @@ python main - close previous server ### contribution guidelines: -- please check that your PR does not break anything \ No newline at end of file +- please check that your PR does not break anything +- unless absolutely nessecary, avoid adding dependecies +- for consistency, use quotation marks, not apostrophes when possible \ No newline at end of file -- 2.39.2 From 7c5f7efc91a2bd5c2ecb1e1dd5e212e703e6b5f4 Mon Sep 17 00:00:00 2001 From: maaa Date: Fri, 14 Jul 2023 02:29:58 +0200 Subject: [PATCH 25/31] server side events --- .gitignore | 3 +- README.md | 2 +- config.ini | 1 + main | 32 ++++++------ requirements.txt | 6 ++- static/css/style.css | 2 +- static/js/chat.js | 116 +++++++++++++++++++++---------------------- 7 files changed, 83 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index 1149575..75f20af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ database.db -uploads \ No newline at end of file +uploads +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md index 206be24..2f7fee2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ burgercat: burger social media ### self hosting: -this guide assumes you have git and python3 installed +this guide assumes you have git, python3, and redis installed on your server ``` git clone https://codeberg.org/burger-software/burgercat diff --git a/config.ini b/config.ini index b4471fb..ff3fe97 100644 --- a/config.ini +++ b/config.ini @@ -4,3 +4,4 @@ SECRET_KEY = placeholder UPLOAD_FOLDER = uploads PASSWORD_REQUIREMENT = 12 UPLOAD_LIMIT = 4 +REDIS_URL = redis://localhost \ No newline at end of file diff --git a/main b/main index 57a5084..bd57b71 100644 --- a/main +++ b/main @@ -7,6 +7,7 @@ import json import secrets import datetime import socket +import threading import subprocess from itertools import groupby from waitress import serve @@ -16,6 +17,8 @@ from werkzeug.middleware.proxy_fix import ProxyFix 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 # read config file config = configparser.ConfigParser() @@ -26,10 +29,13 @@ SECRET_KEY = config["config"]["SECRET_KEY"] UPLOAD_FOLDER = config["config"]["UPLOAD_FOLDER"] UPLOAD_LIMIT = config["config"]["UPLOAD_LIMIT"] PASSWORD_REQUIREMENT = config["config"]["PASSWORD_REQUIREMENT"] +REDIS_URL = config["config"]["REDIS_URL"] app = Flask(__name__) app.config["SECRET_KEY"] = SECRET_KEY +app.config["REDIS_URL"] = REDIS_URL 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) @@ -164,7 +170,7 @@ def chat(): @app.route("/api/chat/listrooms") def chatlistrooms(): conn = get_db_connection() - rooms = conn.execute("SELECT * FROM chatrooms ORDER BY roomname ASC;").fetchall() + rooms = conn.execute("SELECT * FROM chatrooms ORDER BY id ASC;").fetchall() conn.close() template = [] @@ -181,7 +187,7 @@ def chatlistrooms(): @app.route("/api/chat/getmessages/") @limiter.exempt def chatget(roomid): - messages = get_messages(roomid, 40) + messages = get_messages(roomid, 150) template = [] @@ -203,9 +209,6 @@ def chatget(roomid): return(template), 200 -burgerMessageUpdate = True -burgerMessageCache = "" - @app.route("/api/chat/send/", methods=("GET", "POST")) def chatsend(roomid): usersession = request.cookies.get("session_DO_NOT_SHARE") @@ -215,9 +218,6 @@ def chatsend(roomid): data = request.get_json() content = data["content"] - print(content) - print(roomid) - userCookie = get_session(usersession) user = get_user(userCookie["id"]) @@ -226,17 +226,21 @@ def chatsend(roomid): "error": "banned" }, 403 + chatMessageContent = { + "content": content, + "creator": user["username"], + "roomid": roomid, + "created": str(time.time()) + } + + sse.publish({"message": chatMessageContent}, type="publish") + conn = get_db_connection() conn.execute("INSERT INTO chatmessages (content, chatroom_id, creator, created) VALUES (?, ?, ?, ?)", (content, roomid, userCookie["id"], str(time.time()))) conn.commit() conn.close() - global burgerMessageCache - global burgerMessageUpdate - burgerMessageUpdate = True - burgerMessageCache = user["username"] + ": " + content - return "success", 200 @@ -699,6 +703,6 @@ if __name__ == "__main__": sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.bind(('', int(PORT))) - serve(app, sockets=[sock]) + serve(app, sockets=[sock], threads=9999) print("[INFO] Server stopped") diff --git a/requirements.txt b/requirements.txt index 79aec11..ccd3104 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -flask +Flask Flask-Limiter werkzeug waitress -requests \ No newline at end of file +requests +Flask-SSE +APScheduler \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index 7ac1133..3999647 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -246,7 +246,7 @@ body { right: 0; overflow-y: auto; display: flex; - flex-direction: column-reverse; + flex-direction: column; } .messageDiv p { diff --git a/static/js/chat.js b/static/js/chat.js index 1c7194e..eed3f97 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -5,67 +5,56 @@ let statusMessage = document.getElementById("statusMessage") let channelID = 0 -// create a cache to store the images (delete this once you add SSE) -const imageCache = {}; +function addMessage(content, created, creator, roomid) { + const messageParagraph = document.createElement("p"); + const timeParagraph = document.createElement("p"); + + const hideRegex = /(https?:\/\/(?:cdn\.discordapp\.com|media\.discordapp\.net|media\.tenor\.com|i\.imgur\.com)\/.+?\.(?:png|apng|webp|svg|jpg|jpeg|gif))(?=$|\s)/gi; + let messageContent = content.replace(hideRegex, ""); + + messageParagraph.innerText = `${creator}: ${messageContent}`; + messageParagraph.classList.add("messageParagraph"); + messageParagraph.id = "messageParagraph"; + messageParagraph.appendChild(timeParagraph); + + const time = new Intl.DateTimeFormat("en-GB", { hour: "numeric", minute: "numeric" }).format(Number(created.split(".")[0]) * 1000 + 20265); + + messageParagraph.innerHTML = `${time} ${messageParagraph.innerHTML}`; + messageDiv.append(messageParagraph); + + const imgLinks = content?.match(/(https?:\/\/(?:cdn\.discordapp\.com|media\.discordapp\.net|media\.tenor\.com|i\.imgur\.com|burger\.ctaposter\.xyz)\/.+?\.(?:png|apng|webp|svg|jpg|jpeg|gif))(?=$|\s)/gi) || []; + + for (const link of imgLinks) { + const img = new Image(); + img.src = link; + img.className = "messageImage"; + img.onload = () => { + const maxWidth = 400; + const maxHeight = 400; + let { width, height } = img; + if (width > maxWidth || height > maxHeight) { + const ratio = Math.min(maxWidth / width, maxHeight / height); + width *= ratio; + height *= ratio; + } + img.width = width; + img.height = height; + messageParagraph.appendChild(img); + }; + } + messageDiv.scrollTop = messageDiv.scrollHeight - messageDiv.clientHeight; +} async function updateMessages(id) { - try { const response = await fetch(`/api/chat/getmessages/${id}`); const messages = await response.json(); statusMessage.innerText = ""; document.querySelectorAll(".messageParagraph").forEach((el) => el.remove()); - for (const message of messages) { - const messageParagraph = document.createElement("p"); - const timeParagraph = document.createElement("p"); - const { creator, content, id, created } = message; - - // Check if the message content contains any links that are not image links and hide image links - const linkRegex = /(https?:\/\/[^\s]+(?$1"); - - messageParagraph.innerHTML = `${creator.username}: ${messageContent}`; - messageParagraph.classList.add("messageParagraph"); - messageParagraph.id = `messageParagraph${id}`; - messageParagraph.appendChild(timeParagraph); - - const time = new Intl.DateTimeFormat("en-GB", { hour: "numeric", minute: "numeric" }).format(Number(created.split(".")[0]) * 1000 + 20265); - - messageParagraph.innerHTML = `${time} ${messageParagraph.innerHTML}`; - messageDiv.append(messageParagraph); - - const imgLinks = content?.match(/(https?:\/\/(?:cdn\.discordapp\.com|media\.discordapp\.net|media\.tenor\.com|i\.imgur\.com|burger\.ctaposter\.xyz)\/.+?\.(?:png|apng|webp|svg|jpg|jpeg|gif))(?=$|\s)/gi) || []; - - for (const link of imgLinks) { - // delete the code below once you add sse - if (imageCache[link]) { - messageParagraph.appendChild(imageCache[link].cloneNode(true)); - } else { - // delete the code above once you add sse - const img = new Image(); - img.src = link; - img.className = "messageImage"; - img.onload = () => { - const maxWidth = 400; - const maxHeight = 400; - let { width, height } = img; - if (width > maxWidth || height > maxHeight) { - const ratio = Math.min(maxWidth / width, maxHeight / height); - width *= ratio; - height *= ratio; - } - img.width = width; - img.height = height; - // delete the line below once you add sse - imageCache[link] = img.cloneNode(true); - messageParagraph.appendChild(img); - }; - } - } + for (const message of messages.reverse()) { + const { creator, content, roomid, created } = message; + addMessage(content, created, creator["username"], roomid) } - } catch { - statusMessage.innerText = "Not connected"; - } } function selectChannel(id) { @@ -138,20 +127,27 @@ messageBox.addEventListener("keyup", function onEvent(event) { if (!messageBox.value == "") { if (messageBox.value.length < 140) { sendMessage(messageBox.value, channelID) - updateMessages(channelID) messageBox.value = "" } } } }) -function update() { - updateMessages(channelID) - - setTimeout(update, 1500); -} +let messageStream = new EventSource("/stream") window.addEventListener("load", function () { updateRooms() - update() + updateMessages(channelID) + + messageStream.addEventListener("publish", function (event) { + results = JSON.parse(event.data) + + if (Number(results["message"]["roomid"]) == channelID) { + addMessage(results["message"]["content"], results["message"]["created"], results["message"]["creator"], results["message"]["roomid"]) + } + }) +}) + +window.addEventListener("beforeunload", function () { + messageStream.close() }) \ No newline at end of file -- 2.39.2 From 07bbfd9530a20f02868d9ef74e4ce789a669b26c Mon Sep 17 00:00:00 2001 From: maaa Date: Wed, 19 Jul 2023 20:16:13 +0200 Subject: [PATCH 26/31] shutdown --- static/css/style.css | 2 +- static/shutdown.html | 23 +++++++++++++++++++++++ templates/main.html | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 static/shutdown.html diff --git a/static/css/style.css b/static/css/style.css index 3999647..4382dff 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -291,7 +291,7 @@ body { } .warning { - width: 100%; + width: calc(100% - 20px); background-color: #f1b739; color: black; padding: 10px; diff --git a/static/shutdown.html b/static/shutdown.html new file mode 100644 index 0000000..6b607c4 --- /dev/null +++ b/static/shutdown.html @@ -0,0 +1,23 @@ + + + + + burgercat + + + + + + + +
+

burgercat is permanently shutting down on July 21th.

+
+
+

After 19 days of service, burgercat is permanently shutting down on July 21th 2023. 😔

+ After July 21th, you will no longer be able to access burgercat. Please back up anything you want to keep before July 21th.

+

The Codeberg repository will be archived. Third-party instances will not be affected.

+

Thank you for using burgercat, and thank @ffqq, @carsand101, @Mollomm1, and @TestingPlant, @Anonymous for contributing to the project.

+
+ + \ No newline at end of file diff --git a/templates/main.html b/templates/main.html index 2a3932f..8b58fe5 100644 --- a/templates/main.html +++ b/templates/main.html @@ -36,6 +36,7 @@

Your account has been banned. Reason: "{{ userdata.banned }}". Log out

{% endif %} {% endif %} +

⚠️ burgercat will permanently shut down on July 21th. ⚠️ Read more

{% for post in posts %}

{{ getUser(post["creator"])["username"] }}

-- 2.39.2 From faa4dddc53221ec0192a1387eae84f68dc92bc3e Mon Sep 17 00:00:00 2001 From: maaa Date: Wed, 19 Jul 2023 20:21:25 +0200 Subject: [PATCH 27/31] a --- static/shutdown.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/shutdown.html b/static/shutdown.html index 6b607c4..3e42fbc 100644 --- a/static/shutdown.html +++ b/static/shutdown.html @@ -17,7 +17,7 @@

After 19 days of service, burgercat is permanently shutting down on July 21th 2023. 😔

After July 21th, you will no longer be able to access burgercat. Please back up anything you want to keep before July 21th.

The Codeberg repository will be archived. Third-party instances will not be affected.

-

Thank you for using burgercat, and thank @ffqq, @carsand101, @Mollomm1, and @TestingPlant, @Anonymous for contributing to the project.

+

Thank you for using burgercat, and thank @ffqq, @carsand101, @Mollomm1, @TestingPlant, and @Anonymous for contributing to the project.

\ No newline at end of file -- 2.39.2 From 8ca281b1d3045907e9257bf4146e4638ff407400 Mon Sep 17 00:00:00 2001 From: maaa Date: Wed, 19 Jul 2023 20:31:40 +0200 Subject: [PATCH 28/31] add sewn --- static/shutdown.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/shutdown.html b/static/shutdown.html index 3e42fbc..a9b7ee5 100644 --- a/static/shutdown.html +++ b/static/shutdown.html @@ -17,7 +17,7 @@

After 19 days of service, burgercat is permanently shutting down on July 21th 2023. 😔

After July 21th, you will no longer be able to access burgercat. Please back up anything you want to keep before July 21th.

The Codeberg repository will be archived. Third-party instances will not be affected.

-

Thank you for using burgercat, and thank @ffqq, @carsand101, @Mollomm1, @TestingPlant, and @Anonymous for contributing to the project.

+

Thank you for using burgercat, and thank @ffqq, @carsand101, @Mollomm1, @TestingPlant, @sewn, and @Anonymous for contributing to the project.

\ No newline at end of file -- 2.39.2 From e6359db97db87d8edf8aea07de2ec69d532f1e80 Mon Sep 17 00:00:00 2001 From: maaa Date: Mon, 29 Apr 2024 22:36:42 +0200 Subject: [PATCH 29/31] we are so back --- README.md | 6 +- config.ini | 3 +- hashpass.py | 4 + main | 210 +++++++++++++++++++--------------------- requirements.txt | 9 +- static/css/style.css | 27 +++++- static/css/xp.css | 137 -------------------------- static/js/chat.js | 49 +++------- static/shutdown.html | 23 ----- templates/apidocs.html | 9 +- templates/main.html | 9 +- templates/post.html | 24 ++--- templates/settings.html | 9 +- 13 files changed, 173 insertions(+), 346 deletions(-) create mode 100644 hashpass.py delete mode 100644 static/css/xp.css delete mode 100644 static/shutdown.html diff --git a/README.md b/README.md index 2f7fee2..d484197 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ burgercat: burger social media +now back again! + ### self hosting: this guide assumes you have git, python3, and redis installed on your server @@ -10,10 +12,6 @@ python init_db python main ``` -### zero downtime restarts: -- launch new burgercat server -- close previous server - ### contribution guidelines: - please check that your PR does not break anything - unless absolutely nessecary, avoid adding dependecies diff --git a/config.ini b/config.ini index ff3fe97..596f2a9 100644 --- a/config.ini +++ b/config.ini @@ -3,5 +3,4 @@ PORT = 8080 SECRET_KEY = placeholder UPLOAD_FOLDER = uploads PASSWORD_REQUIREMENT = 12 -UPLOAD_LIMIT = 4 -REDIS_URL = redis://localhost \ No newline at end of file +UPLOAD_LIMIT = 12 \ No newline at end of file diff --git a/hashpass.py b/hashpass.py new file mode 100644 index 0000000..d64f4fe --- /dev/null +++ b/hashpass.py @@ -0,0 +1,4 @@ +from werkzeug.security import generate_password_hash, check_password_hash + +passwordthing = input("insert pass: ") +print(generate_password_hash(passwordthing)) \ No newline at end of file diff --git a/main b/main index bd57b71..9ec863f 100644 --- a/main +++ b/main @@ -9,15 +9,13 @@ import datetime import socket import threading import subprocess +import asyncio +from hypercorn.config import Config +from hypercorn.asyncio import serve from itertools import groupby -from waitress import serve from werkzeug.utils import secure_filename from werkzeug.security import generate_password_hash, check_password_hash -from werkzeug.middleware.proxy_fix import ProxyFix -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 quart import Quart, render_template, request, url_for, flash, redirect, session, make_response, send_from_directory, stream_with_context, Response, request, jsonify, websocket from apscheduler.schedulers.background import BackgroundScheduler # read config file @@ -29,23 +27,10 @@ SECRET_KEY = config["config"]["SECRET_KEY"] UPLOAD_FOLDER = config["config"]["UPLOAD_FOLDER"] UPLOAD_LIMIT = config["config"]["UPLOAD_LIMIT"] 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["REDIS_URL"] = REDIS_URL 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": 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() 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): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS - @app.route("/", methods=("GET", "POST")) -def main(): +async def main(): usersession = request.cookies.get("session_DO_NOT_SHARE") conn = get_db_connection() posts = conn.execute("SELECT * FROM posts ORDER BY created DESC;").fetchall() @@ -153,22 +137,22 @@ def main(): if usersession: userCookie = get_session(usersession) 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: - 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")) -def chat(): +async def chat(): usersession = request.cookies.get("session_DO_NOT_SHARE") if usersession: userCookie = get_session(usersession) user = get_user(userCookie["id"]) - return render_template("chat.html", userdata=user) + return await render_template("chat.html", userdata=user) else: - return render_template("chat.html") + return await render_template("chat.html") @app.route("/api/chat/listrooms") -def chatlistrooms(): +async def chatlistrooms(): conn = get_db_connection() rooms = conn.execute("SELECT * FROM chatrooms ORDER BY id ASC;").fetchall() conn.close() @@ -185,8 +169,7 @@ def chatlistrooms(): return(template), 200 @app.route("/api/chat/getmessages/") -@limiter.exempt -def chatget(roomid): +async def chatget(roomid): messages = get_messages(roomid, 150) template = [] @@ -209,13 +192,14 @@ def chatget(roomid): return(template), 200 + @app.route("/api/chat/send/", methods=("GET", "POST")) -def chatsend(roomid): +async def chatsend(roomid): usersession = request.cookies.get("session_DO_NOT_SHARE") if usersession: if request.method == "POST": - data = request.get_json() + data = await request.get_json() content = data["content"] userCookie = get_session(usersession) @@ -233,7 +217,7 @@ def chatsend(roomid): "created": str(time.time()) } - sse.publish({"message": chatMessageContent}, type="publish") + #message_queue.append({"message": chatMessageContent}) conn = get_db_connection() conn.execute("INSERT INTO chatmessages (content, chatroom_id, creator, created) VALUES (?, ?, ?, ?)", @@ -245,7 +229,7 @@ def chatsend(roomid): @app.route("/@", methods=("GET", "POST")) -def user(pageusername): +async def user(pageusername): usersession = request.cookies.get("session_DO_NOT_SHARE") checkusername = check_username_taken(pageusername) @@ -255,20 +239,20 @@ def user(pageusername): if usersession: userCookie = get_session(usersession) 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: - 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: return """""", 404 @app.route("/api/page/", methods=("GET", "POST")) -def apipageuser(userid): +async def apipageuser(userid): pageuser = get_user(userid) addhtml = """ - """ + """ if not pageuser == "error": return addhtml + pageuser["htmldescription"] @@ -276,7 +260,7 @@ def apipageuser(userid): return """""", 404 @app.route("/@/edit", methods=("GET", "POST")) -def edituser(pageusername): +async def edituser(pageusername): usersession = request.cookies.get("session_DO_NOT_SHARE") checkusername = check_username_taken(pageusername) @@ -288,7 +272,9 @@ def edituser(pageusername): user = get_user(userCookie["id"]) if pageuser["username"] == user["username"]: if request.method == "POST": - code = request.form["code"].replace("Content-Security-Policy", "").replace("