forked from lamp/chat
231 lines
7.8 KiB
JavaScript
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;
|
|
}
|
|
} |