1
0
forked from lamp/chat
2022-10-05 21:42:54 -05:00

231 lines
7.8 KiB
JavaScript

require("dotenv").config({path: __dirname+"/.env"});
var MONGODB_URI = process.env.MONGODB_URI || "mongodb://127.0.0.1:27017/chatserver";
var DATA_DIR = process.env.DATA_DIR || "data";
var express = require("express");
var {MongoClient, ObjectId} = require("mongodb");
var socketio = require("socket.io");
var http = require("http");
var busboy = require("busboy");
var path = require("path");
var crypto = require("crypto");
var HashThrough = require("hash-through");
var {to36} = require("1636");
var fs = require("fs");
var mime = require("mime");
var app = express();
app.set("trust proxy", true);
var server = http.createServer(app);
var io = socketio(server, {
cors: {origin: "*"},
maxHttpBufferSize: 16e6,
perMessageDeflate: true
});
var dbclient = new MongoClient(MONGODB_URI);
var db = dbclient.db();
var messages = db.collection("messages");
var emojis = db.collection("emojis");
app.use((req, res, next) => {
console.log(req.ip, req.method, req.url, req.headers["user-agent"]);
next();
});
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "*");
res.header("Access-Control-Allow-Methods", "*");
next();
});
app.get("/messages", (req, res, next) => {
if (req.query.before) var beforeDate = new Date(req.query.before);
if (beforeDate == "Invalid Date") return res.status(400).send("`before` param is invalid date");
if (req.query.after) var afterDate = new Date(req.query.after);
if (afterDate == "Invalid Date") return res.status(400).send("`after` param is invalid date");
if (beforeDate || afterDate) {
var query = {timestamp:{}};
if (beforeDate) query.timestamp.$lt = beforeDate;
if (afterDate) query.timestamp.$gt = afterDate;
}
messages.find(query || {}).project({"file.data": 0, "user.ip": 0}).sort({timestamp: -1}).limit(100).toArray().then(messages => {
res.send(messages.reverse());
}).catch(e => next(e));
});
app.get("/file/:message_id/:filename", async (req, res, next) => {
try {
var doc = await messages.findOne({_id: new ObjectId(req.params.message_id)}, {file:1});
if (!doc) return res.sendStatus(404);
var file = doc.file;
if (!file) return res.status(404).send("message does not have file");
if (req.params.filename != file.name) return res.status(400).send("filename is not correct") //idk
res.type(file.type);
if (res.get("Content-Type").startsWith("text/html")) res.type("text/plain");
res.header("Cache-Control", "max-age=31536000");
res.send(file.data.buffer);
} catch (error) {
next(error);
}
});
app.post("/upload", (req, res, next) => {
var boi = busboy({headers: req.headers});
var codes = [];
boi.on("file", (name, file, info) => {
var resolve;
codes.push(new Promise(r => resolve = r));
var hash = HashThrough(() => crypto.createHash("sha1"));
var tmpfilepath = path.join(DATA_DIR, "tmp", Math.random().toString());
var write = fs.createWriteStream(tmpfilepath);
file.pipe(hash).pipe(write);
write.on("error", error => next(error));
write.on("close", () => {
var code = to36(hash.digest("hex"));
var j = {name, code, type: info.mimeType};
var targetfilepath = path.join(DATA_DIR, "objects", code);
fs.exists(targetfilepath, exists => {
if (exists) {
resolve(j);
fs.unlink(tmpfilepath, e=>e&&console.error(e.stack));
}
else fs.rename(tmpfilepath, targetfilepath, error => {
if (error) console.error(error.stack);
resolve(j);
});
});
});
});
boi.on("close", () => {
Promise.all(codes).then(codes => res.send(codes));
});
req.pipe(boi);
});
app.get("/objects/:code/:filename?", (req, res) => {
//var type = req.query.type?.trim().toLowerCase().replace(/[^a-z0-9\/; =-]/g,'');
//if (type?.startsWith("text/html")) type = type.replace("text/html", "text/plain");
if (req.params.filename) {
var type = mime.getType(req.params.filename);
}
res.header("Cache-Control", "max-age=31536000");
res.sendFile(req.params.code, {
root: path.join(DATA_DIR, "objects/"),
headers: type ? {"Content-Type": type} : undefined
});
});
app.get("/emoji/:emoji", (req, res, next) => {
emojis.findOne({name: req.params.emoji}, function(err, emoji){
if (err) return next(err);
if (!emoji) return res.sendStatus(404);
res.header("Cache-Control", "max-age=31536000");
res.type(emoji.type);
res.send(emoji.data.buffer);
});
});
app.get("/emojis", (req, res, next) => {
emojis.find({}, {name: 1}).toArray(function (err, emojis) {
if (err) return next(err);
emojis = emojis.map(e => e.name);
shuffleArray(emojis);
res.send(emojis);
});
});
app.put("/emoji/:name", express.raw({limit: "1mb", type: ()=>true}), async (req, res, next) => {
try {
var emoji = {
name: req.params.name,
type: req.query.type,
data: req.body
};
if (!emoji.name || !/^[a-z0-9_-]+$/i.test(emoji.name) || !["image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"].includes(emoji.type)) return res.sendStatus(400);
var total = await emojis.countDocuments();
if (total >= 100) return res.status(507).send("too many emojis on the server");
var exist = await emojis.countDocuments({name: emoji.name});
if (exist > 0) return res.status(409).send("emoji already exists with that name");
await emojis.insertOne(emoji);
res.sendStatus(201);
} catch (error) {next(error)}
});
var favicon = fs.readFileSync("logo.svg", "utf-8");
app.get("/favicon.svg", (req, res) => {
res.header("Cache-Control", "max-age=86400");
res.type("svg").send(favicon.replaceAll("{num}", Number(req.query.num || 0) || ""));
});
app.use(express.static(require("path").join(__dirname, "../app/build/")));
dbclient.connect().then(async () => {
io.on("connection", async socket => {
socket.ip = socket.handshake.headers["x-forwarded-for"]?.split(',')[0] || socket.handshake.address;
console.debug("connection from", socket.ip, "socket id", socket.id);
socket.on("user", user => {
user = {
name: user.name?.trim()?.substring(0,32) || "no name",
color: user.color?.trim()?.substring(0,32),
website: user.website?.trim()?.substring(0,1000),
uuid: user.uuid?.substring(0,128) || Math.random().toString(),
socketid: socket.id,
ip: socket.ip,
agent: socket.handshake.headers["user-agent"]
};
console.debug("user", user);
socket.data.user = user;
broadcastUsers();
});
socket.once("user", async user => {
//await newMessage({color: "#00FF00", content:`${user.name} connected`});
socket.on("disconnect", () => {
//newMessage({color: "#FF0000", content: `${socket.data.user.name} disconnected`});
broadcastUsers();
});
socket.on("message", m => newMessage({
content: m.content?.substring(0,10240),
user: Object.assign({}, socket.data.user),
file: m.file ? {
name: m.file.name?.substring(0,128),
data: m.file.data,
type: m.file.type?.substring(0,64)
} : undefined
}));
var history = await messages.find().project({"file.data": 0, "user.ip": 0}).sort({timestamp: -1}).limit(100).toArray();
history = history.reverse();
socket.emit("messages", history);
socket.on("type", () => {
io.emit("type", socket.id);
});
});
});
server.listen(8535);
async function newMessage(message) {
message.timestamp = new Date();
var {insertedId} = await messages.insertOne(message);
message._id = insertedId.toString();
console.debug("message", message);
delete message.file?.data;
delete message.user.ip;
io.emit("message", message);
}
async function broadcastUsers() {
var users = await io.fetchSockets().then(sockets => sockets.filter(socket => socket.data.user).map(socket => {
var user = Object.assign({}, socket.data.user);
delete user.ip;
return user;
}));
io.emit("users", users);
}
});
function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}