server side events
This commit is contained in:
parent
cbbdf37e57
commit
7c5f7efc91
|
@ -1,2 +1,3 @@
|
||||||
database.db
|
database.db
|
||||||
uploads
|
uploads
|
||||||
|
__pycache__
|
|
@ -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
|
||||||
|
|
|
@ -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
32
main
|
@ -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")
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
flask
|
Flask
|
||||||
Flask-Limiter
|
Flask-Limiter
|
||||||
werkzeug
|
werkzeug
|
||||||
waitress
|
waitress
|
||||||
requests
|
requests
|
||||||
|
Flask-SSE
|
||||||
|
APScheduler
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
})
|
})
|
Reference in New Issue