Skip to content

Commit

Permalink
Merge pull request #21 from Chillandchat/base64-to-binary-migration
Browse files Browse the repository at this point in the history
  • Loading branch information
cheng-alvin authored Jun 12, 2023
2 parents 6a7f991 + ec7285b commit bef7385
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 57 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chillandchat/api",
"version": "1.11.4",
"version": "1.11.5",
"main": "./build/js/index.js",
"bin": "./build/js/index.js",
"author": "Alvin cheng <eventide1029@gmail.com>",
Expand All @@ -24,10 +24,12 @@
"express": "^4.17.1",
"express-rate-limit": "^6.3.0",
"file-type": "^16.5.3",
"heic-convert": "^1.2.4",
"mongoose": "^6.7.11",
"node-fetch": "2.6.7",
"nodemailer": "^6.7.3",
"randomcolor": "^0.6.2",
"sanitize-filename": "^1.6.3",
"sharp": "^0.31.2",
"socket.io": "^4.4.1",
"ts-node": "^10.7.0"
Expand Down
31 changes: 31 additions & 0 deletions src/endpoints/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { NextFunction, Request, Response } from "express";

import debug from "../utils/debug";
import sanitizeDirectory from "../utils/sanitizeDirectory";

/**
* This is the content endpoint, this endpoint will send the content to the user once called.
* This endpoint is used to send images, videos, and other files to the user.
*
* @compatible
* @type {GET} The type of request to the endpoint is get.
* @param {string} path The path of the file.
*/

const content = (req: Request, res: Response, _next: NextFunction): void => {
try {
sanitizeDirectory(req.url.slice(8))
.then((cleanPath: any): void => {
res.status(200).sendFile(cleanPath.fullPath, {
root: `${__dirname}/../../user-content`,
});
debug.log(`Content: ${cleanPath.fullPath} sent.`);
})
.catch((err): Response => res.status(403).send(err.toString()));
} catch (err: unknown) {
res.status(500).send(err);
debug.error(err);
}
};

export default content;
90 changes: 90 additions & 0 deletions src/endpoints/legacyUploadContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { NextFunction, Request, Response } from "express";
import fs from "fs";
import sharp from "sharp";
import { exec } from "child_process";
import { FileTypeResult, fromBuffer } from "file-type";

import debug from "../utils/debug";
import user from "../schema/authSchema";
import { AuthSchemaType } from "../utils";

/**
* This is the legacy upload content endpoint, this endpoint uses base-64
* to save the server's chat content in the server.
*
* @note
* Please note that this is the legacy version, and should not be used.
* The only reason this exists is for backward compatibility.
*
* @deprecated
* @type {POST} This is a POST typed endpoint.
* @param {string} id The id of the content.
* @param {string} content The content of the file.
* @param {ContentType} type The type of the file.
* @param {string} user The user who uploaded this content.
*/

const legacyUploadContent = async (
req: Request,
res: Response,
_next: NextFunction
): Promise<void> => {
if (req.query.key !== String(process.env.KEY)) {
res.status(401).send("Invalid api key.");
return;
}

try {
await user
.findOne({ username: { $eq: req.body.user } })
.exec()
.then(async (user: AuthSchemaType): Promise<void> => {
if (user === null) {
res.status(400).send("ERROR: Non-existent user!");
return;
}

const uuid4Regex: RegExp =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89aAbB][0-9a-f]{3}-[0-9a-f]{12}$/i;

if (!uuid4Regex.test(req.body.id) || req.body.user.includes(" ")) {
res
.status(400)
.send("ERROR: Invalid input format, please correct format.");
return;
}

const videoFormat: FileTypeResult | null | undefined =
req.body.type === "CHILL&CHAT_GIF"
? await fromBuffer(Buffer.from(req.body.content, "base64"))
: null;

const fileType: string =
videoFormat !== null ? videoFormat.ext : "webp";
if (!fs.existsSync(`${__dirname}/../../user-content/${req.body.user}`))
fs.mkdirSync(`${__dirname}/../../user-content/${req.body.user}`, {
recursive: true,
});

fs.writeFileSync(
`${__dirname}/../../user-content/${req.body.user}/${req.body.id}.${fileType}`,
req.body.type === "CHILL&CHAT_GIF"
? Buffer.from(req.body.content, "base64")
: await sharp(Buffer.from(req.body.content, "base64"))
.webp({ lossless: true })
.toBuffer(),
"base64"
);

if (req.body.type === "CHILL&CHAT_GIF") {
const command: string = `ffmpeg -ss 00:00:00.000 -i ${__dirname}/../../user-content/${req.body.user}/${req.body.id}.${fileType} -pix_fmt rgb24 -s 320x240 -r 10 -t 00:00:10.000 ${__dirname}/../../user-content/${req.body.user}/${req.body.id}.gif`;
exec(command);
}
});
} catch (err: unknown) {
res.status(500).send(`SERVER ERROR: ${err}`);
debug.error(err);
}
};

export default legacyUploadContent;
122 changes: 76 additions & 46 deletions src/endpoints/uploadContent.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { NextFunction, Request, Response } from "express";
import fs from "fs";
import sharp from "sharp";
import { exec } from "child_process";
import { FileTypeResult, fromBuffer } from "file-type";
import { NextFunction, Request, Response } from "express";
import fs from "fs";
import convert from "heic-convert";

import debug from "../utils/debug";
import user from "../schema/authSchema";
import { AuthSchemaType } from "../utils";

/**
* This is the upload content endpoint, this endpoint will save the content in the server.
*
* @type {POST} This is a POST typed endpoint.
* @param {string} id The id of the content.
* @param {string} content The content of the file.
* @param {ContentType} type The type of the file.
* @param {string} user The user who uploaded this content.
*/
import debug from "../utils/debug";
import sanitizeDirectory from "../utils/sanitizeDirectory";

const uploadContent = async (
req: Request,
Expand All @@ -29,51 +20,90 @@ const uploadContent = async (
}

try {
if (req.query.useBinaryUploadEndpoint !== "true") {
res.redirect(300, "/legacy-endpoints/upload-content");
return;
}

await user
.findOne({ username: { $eq: req.body.user } })
.findOne({ username: { $eq: req.query.user } })
.exec()
.then(async (user: AuthSchemaType): Promise<void> => {
if (user === null) {
res.status(400).send("ERROR: Non-existent user!");
return;
}

const uuid4Regex: RegExp =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89aAbB][0-9a-f]{3}-[0-9a-f]{12}$/i;
let path: any;
const fileExtension: string =
req.query.type === "CHILL&CHAT_IMG" ? "webp" : "gif";

await sanitizeDirectory(
`${req.query.user}/${req.query.id}.${fileExtension}`
)
.then((cleanPath: any): void => (path = cleanPath.fullPath))
.catch((err): Response => res.status(403).send(err));

if (path === undefined) return;

if (!uuid4Regex.test(req.body.id) || req.body.user.includes(" ")) {
res
.status(400)
.send("ERROR: Invalid input format, please correct format.");
if (fs.existsSync(`${__dirname}/../../user-content/${path}`)) {
res.status(400).send("ERROR: File already exists!");
return;
}

const videoFormat: FileTypeResult | null | undefined =
req.body.type === "CHILL&CHAT_GIF"
? await fromBuffer(Buffer.from(req.body.content, "base64"))
: null;

const fileType: string =
videoFormat !== null ? videoFormat.ext : "webp";
if (!fs.existsSync(`${__dirname}/../../user-content/${req.body.user}`))
fs.mkdirSync(`${__dirname}/../../user-content/${req.body.user}`, {
recursive: true,
});

fs.writeFileSync(
`${__dirname}/../../user-content/${req.body.user}/${req.body.id}.${fileType}`,
req.body.type === "CHILL&CHAT_GIF"
? Buffer.from(req.body.content, "base64")
: await sharp(Buffer.from(req.body.content, "base64"))
.webp({ lossless: true })
.toBuffer(),
"base64"
);

if (req.body.type === "CHILL&CHAT_GIF") {
const command: string = `ffmpeg -ss 00:00:00.000 -i ${__dirname}/../../user-content/${req.body.user}/${req.body.id}.${fileType} -pix_fmt rgb24 -s 320x240 -r 10 -t 00:00:10.000 ${__dirname}/../../user-content/${req.body.user}/${req.body.id}.gif`;
exec(command);
if (req.headers["content-type"] !== "application/octet-stream") {
res.status(400).send("ERROR: Invalid content type.");
return;
}

switch (req.query.type) {
case "CHILL&CHAT_IMG":
const heic: boolean = req.body
.slice(0, 16)
.toString("utf-8")
.includes("heic");

const buffer: any = !heic
? req.body
: await convert({
buffer: req.body,
format: "JPEG",
quality: 1,
});

fs.writeFileSync(
`${__dirname}/../../user-content/${path}`,
await sharp(buffer).webp({ lossless: true }).toBuffer()
);
break;

case "CHILL&CHAT_GIF":
fs.writeFileSync(
`${__dirname}/../../user-content/${path.slice(path.length - 4)}`,
req.body
);

const command: string = `ffmpeg -i ${__dirname}/../../user-content/${path.slice(
path.length - 4
)} -pix_fmt rgb8 ${__dirname}/../../user-content/${path}`;
exec(command, (_err: unknown): void => {
fs.unlinkSync(
`${__dirname}/../../user-content/${path.slice(path.length - 4)}`
);
});
break;

default:
res
.status(400)
.send(
"ERROR: Invalid file type, please provide correct file type."
);
return;
}

res.status(201).send("Successfully saved file.");
debug.log(`Successfully saved upload item: /user-content/${path}`);
});
} catch (err: unknown) {
res.status(500).send(`SERVER ERROR: ${err}`);
Expand Down
29 changes: 19 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@
import { Server, Socket } from "socket.io";
import { createServer } from "http";
import express from "express";
import path from "path";
import dotenv from "dotenv";
import http from "http";
import https from "https";
import compression from "compression";

import contentEndpoint from "./endpoints/content";
import { MessageSchemaType } from "./utils";
import home from "./endpoints/home";
import getMessages from "./endpoints/getMessages";
Expand All @@ -59,14 +59,15 @@ import followUser from "./endpoints/followUser";
import updateDescription from "./endpoints/updateDescription";
import updateIconColor from "./endpoints/updateIconColor";
import getPublicRooms from "./endpoints/getPublicRooms";
import uploadContent from "./endpoints/uploadContent";
import legacyUploadContent from "./endpoints/legacyUploadContent";
import content from "./schema/contentSchema";
import connectDatabase from "./utils/connectDatabase";
import getGif from "./endpoints/getGif";
import deleteUser from "./endpoints/deleteUser";
import verifyClient from "./endpoints/verifyClient";
import sendNotifications from "./utils/sendNotification";
import uploadToken from "./endpoints/uploadToken";
import uploadContent from "./endpoints/uploadContent";

const app: express.Express = express();
const httpServer: any = createServer(app);
Expand All @@ -90,41 +91,49 @@ https.globalAgent.maxSockets = Infinity;
connectDatabase();

app.use(express.json({ limit: "10mb" }));
app.use(apiLimiter);

app.use(
"/content",
express.static(path.join(__dirname, "../user-content/"), { maxAge: 31557600 })
rateLimit({
windowMs: 1 * 30 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
})
);

app.use(compression());

app.get("/", home);
app.post("/api/signup", signup);
app.get("/site-map", siteMap);

app.post("/api/login", login);
app.get("/api/get-messages", getMessages);
app.get("/site-map", siteMap);
app.get("/api/get-users", getUsers); // Deprecated.
app.get("/api/get-user-info", getUserInfo);
app.get("/api/get-rooms", getAllRooms);
app.get("/api/verify-client", verifyClient);
app.get("/api/get-gif", getGif);
app.get("/api/get-public-rooms", getPublicRooms);
app.get("/content*/", contentEndpoint);

app.post("/api/delete-user", deleteUser);
app.post("/api/search-message", searchMessage);
app.post("/api/block_user", blockUser);
app.post("/api/upload-content", uploadContent);
app.post("/api/create-room", createRoom);
app.post("/api/join-room", joinRoom);
app.post("/api/report-room", reportRoom);
app.post("/api/remove-room", removeRoom);
app.post("/api/unfollow-user", unfollowUser);
app.post("/api/signup", signup);
app.post("/api/follow-user", followUser);
app.post("/api/update-description", updateDescription);
app.post("/api/update-icon-color", updateIconColor);
app.post("/api/upload-token", uploadToken);
app.post("/api/upload-content", express.raw({ limit: "100mb" }), uploadContent);

/** ---------------------- @deprecated ---------------------- */
app.get("/legacy-endpoints/get-users", getUsers);
app.post("/legacy-endpoints/upload-content", legacyUploadContent);

// Socket server:
io.on("connection", (socket: Socket): void => {
socket.on(
"server-message",
Expand Down
Loading

0 comments on commit bef7385

Please sign in to comment.