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