diff --git a/package-lock.json b/package-lock.json index 9d7a1f51..9b5a5310 100644 --- a/package-lock.json +++ b/package-lock.json @@ -129,6 +129,12 @@ "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz", "integrity": "sha512-flgpHJjntpBAdJD43ShRosQvNC0ME97DCfGvZEDlAThQmnerRXrLbX6YgzRBQCZTthET9eAWFAMaYP0m0Y4HzQ==" }, + "@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", + "dev": true + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -968,6 +974,11 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" } } }, @@ -1457,9 +1468,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "vary": { "version": "1.1.2", diff --git a/package.json b/package.json index 847c688f..03662356 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,15 @@ "lambert-db": "^1.0.5", "missing-native-js-functions": "^1.0.8", "multer": "^1.4.2", - "node-fetch": "^2.6.1" + "node-fetch": "^2.6.1", + "uuid": "^8.3.2" }, "devDependencies": { "@types/btoa": "^1.2.3", "@types/express": "^4.17.9", "@types/multer": "^1.4.5", "@types/node": "^14.14.16", - "@types/node-fetch": "^2.5.7" + "@types/node-fetch": "^2.5.7", + "@types/uuid": "^8.3.0" } } diff --git a/src/Snowflake.js b/src/Snowflake.js new file mode 100644 index 00000000..feb5eb41 --- /dev/null +++ b/src/Snowflake.js @@ -0,0 +1,145 @@ +// @ts-nocheck + +// github.com/discordjs/discord.js/blob/master/src/util/Snowflake.js +"use strict"; + +// Discord epoch (2015-01-01T00:00:00.000Z) +const EPOCH = 1420070400000; +let INCREMENT = 0; + +/** + * A container for useful snowflake-related methods. + */ +class SnowflakeUtil { + constructor() { + throw new Error(`The ${this.constructor.name} class may not be instantiated.`); + } + + /** + * A Twitter snowflake, except the epoch is 2015-01-01T00:00:00.000Z + * ``` + * If we have a snowflake '266241948824764416' we can represent it as binary: + * + * 64 22 17 12 0 + * 000000111011000111100001101001000101000000 00001 00000 000000000000 + * number of ms since Discord epoch worker pid increment + * ``` + * @typedef {string} Snowflake + */ + + /** + * Transforms a snowflake from a decimal string to a bit string. + * @param {Snowflake} num Snowflake to be transformed + * @returns {string} + * @private + */ + static idToBinary(num) { + let bin = ""; + let high = parseInt(num.slice(0, -10)) || 0; + let low = parseInt(num.slice(-10)); + while (low > 0 || high > 0) { + bin = String(low & 1) + bin; + low = Math.floor(low / 2); + if (high > 0) { + low += 5000000000 * (high % 2); + high = Math.floor(high / 2); + } + } + return bin; + } + + /** + * Transforms a snowflake from a bit string to a decimal string. + * @param {string} num Bit string to be transformed + * @returns {Snowflake} + * @private + */ + static binaryToID(num) { + let dec = ""; + + while (num.length > 50) { + const high = parseInt(num.slice(0, -32), 2); + const low = parseInt((high % 10).toString(2) + num.slice(-32), 2); + + dec = (low % 10).toString() + dec; + num = + Math.floor(high / 10).toString(2) + + Math.floor(low / 10) + .toString(2) + .padStart(32, "0"); + } + + num = parseInt(num, 2); + while (num > 0) { + dec = (num % 10).toString() + dec; + num = Math.floor(num / 10); + } + + return dec; + } + + /** + * Generates a Discord snowflake. + * This hardcodes the worker ID as 1 and the process ID as 0. + * @param {number|Date} [timestamp=Date.now()] Timestamp or date of the snowflake to generate + * @returns {Snowflake} The generated snowflake + */ + static generate(timestamp = Date.now()) { + if (timestamp instanceof Date) timestamp = timestamp.getTime(); + if (typeof timestamp !== "number" || isNaN(timestamp)) { + throw new TypeError( + `"timestamp" argument must be a number (received ${isNaN(timestamp) ? "NaN" : typeof timestamp})` + ); + } + if (INCREMENT >= 4095) INCREMENT = 0; + const BINARY = `${(timestamp - EPOCH).toString(2).padStart(42, "0")}0000100000${(INCREMENT++) + .toString(2) + .padStart(12, "0")}`; + return SnowflakeUtil.binaryToID(BINARY); + } + + /** + * A deconstructed snowflake. + * @typedef {Object} DeconstructedSnowflake + * @property {number} timestamp Timestamp the snowflake was created + * @property {Date} date Date the snowflake was created + * @property {number} workerID Worker ID in the snowflake + * @property {number} processID Process ID in the snowflake + * @property {number} increment Increment in the snowflake + * @property {string} binary Binary representation of the snowflake + */ + + /** + * Deconstructs a Discord snowflake. + * @param {Snowflake} snowflake Snowflake to deconstruct + * @returns {DeconstructedSnowflake} Deconstructed snowflake + */ + static deconstruct(snowflake) { + const BINARY = SnowflakeUtil.idToBinary(snowflake).toString(2).padStart(64, "0"); + const res = { + timestamp: parseInt(BINARY.substring(0, 42), 2) + EPOCH, + workerID: parseInt(BINARY.substring(42, 47), 2), + processID: parseInt(BINARY.substring(47, 52), 2), + increment: parseInt(BINARY.substring(52, 64), 2), + binary: BINARY, + }; + Object.defineProperty(res, "date", { + get: function get() { + return new Date(this.timestamp); + }, + enumerable: true, + }); + return res; + } + + /** + * Discord's epoch value (2015-01-01T00:00:00.000Z). + * @type {number} + * @readonly + */ + static get EPOCH() { + return EPOCH; + } +} + +module.exports = SnowflakeUtil; diff --git a/src/routes/attachments.ts b/src/routes/attachments.ts new file mode 100644 index 00000000..7d09e402 --- /dev/null +++ b/src/routes/attachments.ts @@ -0,0 +1,50 @@ +import { Router } from "express"; +import multer from "multer"; +import Snowflake from "../Snowflake"; + +const multer_ = multer(); +const router = Router(); + +type Attachment = { + filename: string; + file: string; + id: string; + type: string; +}; + +router.post("/:filename", multer_.single("attachment"), async (req, res) => { + const { buffer, mimetype } = req.file; + const { filename } = req.params; + const { db } = req.server; + + const File: Attachment = { + filename, + file: buffer.toString("base64"), + id: Snowflake.generate(), + type: mimetype, + }; + + if (!(await db.data.attachments.push(File))) throw new Error("Error uploading file"); + + return res.status(201).send({ success: true, message: "attachment uploaded", id: File.id, filename }); +}); + +router.get("/:hash/:filename", async (req, res) => { + const { db } = req.server; + const { hash, filename } = req.params; + + const File: Attachment = await db.data.attachments({ id: hash, filename: filename }).get(); + + res.set("Content-Type", File.type); + return res.send(Buffer.from(File.file, "base64")); +}); + +router.delete("/:hash/:filename", async (req, res) => { + const { hash, filename } = req.params; + const { db } = req.server; + + await db.data.attachments({ id: hash, filename: filename }).delete(); + return res.send({ success: true, message: "attachment deleted" }); +}); + +export default router; diff --git a/src/routes/attachments.ts.disabled b/src/routes/attachments.ts.disabled deleted file mode 100644 index db1a7efc..00000000 --- a/src/routes/attachments.ts.disabled +++ /dev/null @@ -1,19 +0,0 @@ -import { Router } from "express"; -import multer from "multer"; -const multer_ = multer(); - -const router = Router(); -router.post("/:file", multer_.single("attachment"), async (req, res) => { - const { buffer } = req.file; - - res.set("Content-Type", "image/png"); - res.send(buffer); -}); -router.get("/:hash/:file", async (req, res) => { - res.send(`${req.params.hash}/${req.params.file}`); -}); -router.delete("/:hash/:file", async (req, res) => { - res.send("remove"); -}); - -export default router;