server side events

This commit is contained in:
maaa 2023-07-14 02:29:58 +02:00
parent cbbdf37e57
commit 7c5f7efc91
7 changed files with 83 additions and 79 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
database.db database.db
uploads uploads
__pycache__

View File

@ -1,7 +1,7 @@
burgercat: burger social media burgercat: burger social media
### self hosting: ### 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 git clone https://codeberg.org/burger-software/burgercat

View File

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

32
main
View File

@ -7,6 +7,7 @@ import json
import secrets import secrets
import datetime import datetime
import socket import socket
import threading
import subprocess import subprocess
from itertools import groupby from itertools import groupby
from waitress import serve 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 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 import Limiter
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
from flask_sse import sse
from apscheduler.schedulers.background import BackgroundScheduler
# read config file # read config file
config = configparser.ConfigParser() config = configparser.ConfigParser()
@ -26,10 +29,13 @@ 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 = Flask(__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) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
@ -164,7 +170,7 @@ def chat():
@app.route("/api/chat/listrooms") @app.route("/api/chat/listrooms")
def chatlistrooms(): def chatlistrooms():
conn = get_db_connection() 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() conn.close()
template = [] template = []
@ -181,7 +187,7 @@ def chatlistrooms():
@app.route("/api/chat/getmessages/<roomid>") @app.route("/api/chat/getmessages/<roomid>")
@limiter.exempt @limiter.exempt
def chatget(roomid): def chatget(roomid):
messages = get_messages(roomid, 40) messages = get_messages(roomid, 150)
template = [] template = []
@ -203,9 +209,6 @@ def chatget(roomid):
return(template), 200 return(template), 200
burgerMessageUpdate = True
burgerMessageCache = ""
@app.route("/api/chat/send/<roomid>", methods=("GET", "POST")) @app.route("/api/chat/send/<roomid>", methods=("GET", "POST"))
def chatsend(roomid): def chatsend(roomid):
usersession = request.cookies.get("session_DO_NOT_SHARE") usersession = request.cookies.get("session_DO_NOT_SHARE")
@ -215,9 +218,6 @@ def chatsend(roomid):
data = request.get_json() data = request.get_json()
content = data["content"] content = data["content"]
print(content)
print(roomid)
userCookie = get_session(usersession) userCookie = get_session(usersession)
user = get_user(userCookie["id"]) user = get_user(userCookie["id"])
@ -226,17 +226,21 @@ def chatsend(roomid):
"error": "banned" "error": "banned"
}, 403 }, 403
chatMessageContent = {
"content": content,
"creator": user["username"],
"roomid": roomid,
"created": str(time.time())
}
sse.publish({"message": chatMessageContent}, type="publish")
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 (?, ?, ?, ?)",
(content, roomid, userCookie["id"], str(time.time()))) (content, roomid, userCookie["id"], str(time.time())))
conn.commit() conn.commit()
conn.close() conn.close()
global burgerMessageCache
global burgerMessageUpdate
burgerMessageUpdate = True
burgerMessageCache = user["username"] + ": " + content
return "success", 200 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_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
sock.bind(('', int(PORT))) sock.bind(('', int(PORT)))
serve(app, sockets=[sock]) serve(app, sockets=[sock], threads=9999)
print("[INFO] Server stopped") print("[INFO] Server stopped")

View File

@ -1,5 +1,7 @@
flask Flask
Flask-Limiter Flask-Limiter
werkzeug werkzeug
waitress waitress
requests requests
Flask-SSE
APScheduler

View File

@ -246,7 +246,7 @@ body {
right: 0; right: 0;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column;
} }
.messageDiv p { .messageDiv p {

View File

@ -5,67 +5,56 @@ let statusMessage = document.getElementById("statusMessage")
let channelID = 0 let channelID = 0
// create a cache to store the images (delete this once you add SSE) function addMessage(content, created, creator, roomid) {
const imageCache = {}; 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 = `<span style="color: #515051; font-size: 14px;">${time}</span> ${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) { async function updateMessages(id) {
try {
const response = await fetch(`/api/chat/getmessages/${id}`); const response = await fetch(`/api/chat/getmessages/${id}`);
const messages = await response.json(); const messages = await response.json();
statusMessage.innerText = ""; statusMessage.innerText = "";
document.querySelectorAll(".messageParagraph").forEach((el) => el.remove()); document.querySelectorAll(".messageParagraph").forEach((el) => el.remove());
for (const message of messages) { for (const message of messages.reverse()) {
const messageParagraph = document.createElement("p"); const { creator, content, roomid, created } = message;
const timeParagraph = document.createElement("p"); addMessage(content, created, creator["username"], roomid)
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]+(?<!\.(?:png|apng|webp|svg|jpg|jpeg|gif)))(?=\s|$)|(?<=\s|^)(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;
let messageContent = content.replace(linkRegex, "<a href='$1' target='_blank'>$1</a>");
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 = `<span style="color: #515051; font-size: 14px;">${time}</span> ${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);
};
}
}
} }
} catch {
statusMessage.innerText = "Not connected";
}
} }
function selectChannel(id) { function selectChannel(id) {
@ -138,20 +127,27 @@ messageBox.addEventListener("keyup", function onEvent(event) {
if (!messageBox.value == "") { if (!messageBox.value == "") {
if (messageBox.value.length < 140) { if (messageBox.value.length < 140) {
sendMessage(messageBox.value, channelID) sendMessage(messageBox.value, channelID)
updateMessages(channelID)
messageBox.value = "" messageBox.value = ""
} }
} }
} }
}) })
function update() { let messageStream = new EventSource("/stream")
updateMessages(channelID)
setTimeout(update, 1500);
}
window.addEventListener("load", function () { window.addEventListener("load", function () {
updateRooms() 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()
}) })