@vex-chat/spire 0.7.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -26
- package/dist/ClientManager.d.ts +24 -25
- package/dist/ClientManager.js +232 -509
- package/dist/ClientManager.js.map +1 -0
- package/dist/Database.d.ts +49 -41
- package/dist/Database.js +698 -716
- package/dist/Database.js.map +1 -0
- package/dist/Spire.d.ts +23 -15
- package/dist/Spire.js +518 -218
- package/dist/Spire.js.map +1 -0
- package/dist/__tests__/Database.spec.js +113 -73
- package/dist/__tests__/Database.spec.js.map +1 -0
- package/dist/db/schema.d.ts +134 -0
- package/dist/db/schema.js +2 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -5
- package/dist/index.js.map +1 -0
- package/dist/middleware/validate.d.ts +12 -0
- package/dist/middleware/validate.js +35 -0
- package/dist/middleware/validate.js.map +1 -0
- package/dist/migrations/2026-04-06_initial-schema.d.ts +3 -0
- package/dist/migrations/2026-04-06_initial-schema.js +192 -0
- package/dist/migrations/2026-04-06_initial-schema.js.map +1 -0
- package/dist/run.js +26 -21
- package/dist/run.js.map +1 -0
- package/dist/server/avatar.d.ts +3 -4
- package/dist/server/avatar.js +85 -67
- package/dist/server/avatar.js.map +1 -0
- package/dist/server/errors.d.ts +59 -0
- package/dist/server/errors.js +94 -0
- package/dist/server/errors.js.map +1 -0
- package/dist/server/file.d.ts +3 -4
- package/dist/server/file.js +101 -61
- package/dist/server/file.js.map +1 -0
- package/dist/server/index.d.ts +9 -6
- package/dist/server/index.js +595 -70
- package/dist/server/index.js.map +1 -0
- package/dist/server/invite.d.ts +4 -5
- package/dist/server/invite.js +21 -103
- package/dist/server/invite.js.map +1 -0
- package/dist/server/openapi.d.ts +2 -0
- package/dist/server/openapi.js +40 -0
- package/dist/server/openapi.js.map +1 -0
- package/dist/server/permissions.d.ts +16 -0
- package/dist/server/permissions.js +22 -0
- package/dist/server/permissions.js.map +1 -0
- package/dist/server/rateLimit.d.ts +28 -0
- package/dist/server/rateLimit.js +58 -0
- package/dist/server/rateLimit.js.map +1 -0
- package/dist/server/user.d.ts +4 -7
- package/dist/server/user.js +66 -76
- package/dist/server/user.js.map +1 -0
- package/dist/server/utils.d.ts +35 -7
- package/dist/server/utils.js +50 -6
- package/dist/server/utils.js.map +1 -0
- package/dist/types/express.d.ts +20 -0
- package/dist/types/express.js +2 -0
- package/dist/types/express.js.map +1 -0
- package/dist/utils/createLogger.js +13 -19
- package/dist/utils/createLogger.js.map +1 -0
- package/dist/utils/createUint8UUID.js +6 -10
- package/dist/utils/createUint8UUID.js.map +1 -0
- package/dist/utils/jwtSecret.d.ts +7 -0
- package/dist/utils/jwtSecret.js +15 -0
- package/dist/utils/jwtSecret.js.map +1 -0
- package/dist/utils/loadEnv.js +7 -22
- package/dist/utils/loadEnv.js.map +1 -0
- package/dist/utils/msgpack.d.ts +2 -0
- package/dist/utils/msgpack.js +4 -0
- package/dist/utils/msgpack.js.map +1 -0
- package/package.json +91 -63
- package/src/ClientManager.ts +434 -0
- package/src/Database.ts +925 -0
- package/src/Spire.ts +878 -0
- package/src/__tests__/Database.spec.ts +167 -0
- package/src/ambient-modules.d.ts +1 -0
- package/src/db/schema.ts +165 -0
- package/src/index.ts +3 -0
- package/src/middleware/validate.ts +38 -0
- package/src/migrations/2026-04-06_initial-schema.ts +218 -0
- package/src/run.ts +37 -0
- package/src/server/avatar.ts +141 -0
- package/src/server/errors.ts +133 -0
- package/src/server/file.ts +172 -0
- package/src/server/index.ts +855 -0
- package/src/server/invite.ts +65 -0
- package/src/server/openapi.ts +51 -0
- package/src/server/permissions.ts +40 -0
- package/src/server/rateLimit.ts +86 -0
- package/src/server/user.ts +125 -0
- package/src/server/utils.ts +59 -0
- package/src/types/express.ts +23 -0
- package/src/utils/createLogger.ts +47 -0
- package/src/utils/createUint8UUID.ts +9 -0
- package/src/utils/jwtSecret.ts +16 -0
- package/src/utils/loadEnv.ts +15 -0
- package/src/utils/msgpack.ts +4 -0
- package/avatars/169d76cb-6e7c-4e24-8224-017673eed8ff +0 -0
- package/avatars/1cae8d0b-0c6b-4c73-b25c-2d2349a57122 +0 -0
- package/avatars/1d87bc2b-71fc-4818-8004-40d04093e5fc +0 -0
- package/avatars/1f2c3d62-8b4d-465a-9895-51a24caef00d +0 -0
- package/avatars/245ee7fc-1004-41ab-adab-a04c9ceb9d7a +0 -0
- package/avatars/2465c28c-bdaf-4fa2-b42a-d054f04dc39b +0 -0
- package/avatars/3900a674-a2dd-4996-a61d-8c29b3270f41 +0 -0
- package/avatars/3c3b9c77-ea50-45e7-bb25-65d6f3d2a219 +0 -0
- package/avatars/414a2ff4-ad2f-4a7d-aa27-3b09ad3522b2 +0 -0
- package/avatars/522fe504-f0ad-4ed4-9dc6-e5dc2338e531 +0 -0
- package/avatars/53e5eb29-e7d1-4d58-add9-d44f39f0cfb7 +0 -0
- package/avatars/54d3f757-1038-41c8-bfb9-efd37b6e8ebe +0 -0
- package/avatars/623e86d7-c49c-46f6-9b76-ca70c9dbc43b +0 -0
- package/avatars/66e2abae-60f5-4dd1-b9fb-297a4bedfeb0 +0 -0
- package/avatars/6f37980e-f6fa-4d6d-9206-24050b403f45 +0 -0
- package/avatars/80138ece-eb5c-4b20-817b-903d6b0f54ae +0 -0
- package/avatars/841c77d3-37c4-431b-be22-672888062874 +0 -0
- package/avatars/88051a61-2bda-4750-95a3-5fb0b4918149 +0 -0
- package/avatars/89540973-c421-4bf1-8b89-9f1eaa51c1b5 +0 -0
- package/avatars/8a802fad-8c99-4942-8f80-47cec600149c +0 -0
- package/avatars/90531d8a-907a-4a1a-ac45-c85e4acb0df9 +0 -0
- package/avatars/9b7d0da9-b8d6-4801-b128-9993f79f464a +0 -0
- package/avatars/9bc456f1-c4c4-48a1-b9e6-fd44dd744a72 +0 -0
- package/avatars/9cf878bf-7430-49ec-a47a-78f3c93793dd +0 -0
- package/avatars/9ee82847-6ad3-45e5-92b9-f474a6c54d96 +0 -0
- package/avatars/ab44c857-d81d-4c88-85db-32f9532e5376 +0 -0
- package/avatars/b396a8d2-dc14-48d2-aac4-3755dc637051 +0 -0
- package/avatars/b6ac11c5-a8b2-4e0a-995a-9c87f1e58787 +0 -0
- package/avatars/b79d6855-b738-434c-be32-809637e62b9b +0 -0
- package/avatars/bbcd0188-d6a5-48ae-90fb-be5ff30599ab +0 -0
- package/avatars/bc7a9e0e-4720-4a6e-a90d-c11fec94d380 +0 -0
- package/avatars/c1c4889f-8383-4041-8bdd-9fded4046f37 +0 -0
- package/avatars/c4c7203c-d93a-4749-ade2-17053acf1d2a +0 -0
- package/avatars/ca974a70-0a23-4668-8b80-c4304dc7f793 +0 -0
- package/avatars/cf119a0d-eb3f-4bed-905b-f14a876c3535 +0 -0
- package/avatars/d464b03d-30c2-49e3-a666-80aefa8a1b35 +0 -0
- package/avatars/da0eee89-82f0-4d45-ab48-7d2786b634c5 +0 -0
- package/avatars/de4c77a5-68e9-4bb2-b40d-d964bf377d61 +0 -0
- package/avatars/dea95395-7d0b-42aa-a9ed-40c7d4fb4c48 +0 -0
- package/avatars/edb30749-59ba-4aa2-9c52-0fb22048f4cf +0 -0
- package/avatars/f17c245a-af7d-445b-9365-49f7f54b1eeb +0 -0
- package/avatars/f1ee6a35-b262-4dbf-99f5-3d011e3b98ec +0 -0
- package/avatars/f802bdd0-345d-41f6-9184-0f30e1258fb3 +0 -0
- package/dist/migrations/20210103192527_users.d.ts +0 -3
- package/dist/migrations/20210103192527_users.js +0 -30
- package/dist/migrations/20210103193502_mail.d.ts +0 -3
- package/dist/migrations/20210103193502_mail.js +0 -35
- package/dist/migrations/20210103193525_preKeys.d.ts +0 -3
- package/dist/migrations/20210103193525_preKeys.js +0 -30
- package/dist/migrations/20210103193553_oneTimeKeys.d.ts +0 -3
- package/dist/migrations/20210103193553_oneTimeKeys.js +0 -30
- package/dist/migrations/20210103193615_servers.d.ts +0 -3
- package/dist/migrations/20210103193615_servers.js +0 -28
- package/dist/migrations/20210103193729_channels.d.ts +0 -3
- package/dist/migrations/20210103193729_channels.js +0 -28
- package/dist/migrations/20210103193749_permissions.d.ts +0 -3
- package/dist/migrations/20210103193749_permissions.js +0 -30
- package/dist/migrations/20210103193801_files.d.ts +0 -3
- package/dist/migrations/20210103193801_files.js +0 -28
- package/files/00ea7368-45a7-4f6a-a199-974da14be1a0 +0 -0
- package/files/039503d2-a170-4962-b921-c97994ba64ff +0 -0
- package/files/14b6fa02-4cbb-40df-be4f-a07187cb619e +0 -0
- package/files/15c04cb1-dc6a-4f19-aa6f-4a3b92d05bf7 +0 -0
- package/files/2a8d411c-8b92-4532-b84d-d64c638d6293 +0 -0
- package/files/37de2cd3-08a8-4044-9c37-e13386765f3d +0 -0
- package/files/42452029-284e-4c81-9f18-feb6ce309eed +0 -0
- package/files/43d09f2f-29c8-415f-8c4a-23f2a32eb79e +0 -0
- package/files/52992923-33ab-44a1-8118-605e9b4856a7 +0 -0
- package/files/53180681-36e2-49c0-8382-94dca0da09bf +0 -0
- package/files/5a56cd7b-1d04-4619-b60f-1e5515b9164a +0 -0
- package/files/5ced7676-20c4-4219-a4f2-70a25eb7eea8 +0 -0
- package/files/60e787ff-8ec5-444d-b963-0aaf5313b53e +0 -0
- package/files/67a17729-fcb7-4339-9499-1fc08fea72ca +0 -0
- package/files/68d09565-908d-4a67-8f09-e183f8708eb4 +0 -0
- package/files/70e587c5-56e8-47f0-bd36-691efcb0cc2e +0 -4
- package/files/7a227619-b715-4e2c-a79d-b2158d56a799 +0 -0
- package/files/7e3cc3ea-b706-4835-994d-65d33acaf369 +0 -0
- package/files/816f72a0-65dc-40c1-8862-1d22065cca3c +0 -0
- package/files/8bf84972-5086-4631-a752-093d7b1a098b +0 -0
- package/files/8c46e3bc-3f2e-441d-b8cc-17428fc3d219 +0 -0
- package/files/8eb83364-8826-4eee-895a-ba5cd3ab85e9 +0 -0
- package/files/8faefaea-14e3-49e4-ac74-9710622bfae9 +0 -0
- package/files/91af08c2-9dca-41f4-b6c6-b7bf33505df9 +0 -0
- package/files/9349dffa-35dd-49a0-a651-10b438038f95 +0 -0
- package/files/b0a12a41-6283-4f27-b2c8-66774991d96c +0 -0
- package/files/b28afb08-d18b-48a8-93c3-6a59c66d8fee +0 -0
- package/files/be60fe01-1578-4363-a908-41500092b77d +0 -0
- package/files/cf7b90e3-734a-4453-9ff3-9a331404b5ef +0 -0
- package/files/d1be30aa-8ff4-41c1-b360-f65922029047 +0 -0
- package/files/d2d31a57-a413-443c-9b97-fa9ca394ef0b +0 -0
- package/files/d357f223-b786-478d-a113-b53cb62acdab +0 -0
- package/files/d4a69ee0-05c4-4ff0-8753-624cdd0f24e9 +0 -0
- package/files/d57f411b-1874-4f00-a6b0-2f86de6b229d +0 -0
- package/files/d85aaa33-7f70-4a70-b3d2-cad667beb38c +0 -0
- package/files/db2fc236-dbe0-4d24-bfaf-884829fd090a +0 -0
- package/files/df2e20ec-6dcc-4402-94ee-7d1163f84197 +0 -0
- package/files/e0880ab5-6d49-4dc0-a5ec-0d3793a8a7b8 +0 -0
- package/files/e59ee9c6-5aea-48ea-b3d9-94a324dc2260 +0 -0
- package/files/e6d7aad6-4220-4ce7-85f8-89d0b1f0cc89 +0 -0
- package/files/f0bfc98c-3534-459d-931a-7c6e77bde153 +0 -0
- package/files/f8443740-050c-4714-9bf6-334783ae6ffd +0 -0
- package/files/fc405ae7-f9fb-455d-ac8c-0ca0d88d4115 +0 -0
- package/jest.config.js +0 -13
- package/spire.sqlite +0 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
import type { Database } from "../Database.ts";
|
|
2
|
+
import type { Emoji } from "@vex-chat/types";
|
|
3
|
+
import type winston from "winston";
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as fsp from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
import express from "express";
|
|
9
|
+
|
|
10
|
+
import { XUtils } from "@vex-chat/crypto";
|
|
11
|
+
import { type KeyPair, xSignOpen } from "@vex-chat/crypto";
|
|
12
|
+
import { PreKeysWSSchema, TokenScopes, UserSchema } from "@vex-chat/types";
|
|
13
|
+
|
|
14
|
+
import cors from "cors";
|
|
15
|
+
import { fileTypeFromBuffer, fileTypeFromFile } from "file-type";
|
|
16
|
+
import helmet from "helmet";
|
|
17
|
+
import jwt from "jsonwebtoken";
|
|
18
|
+
import morgan from "morgan";
|
|
19
|
+
import multer from "multer";
|
|
20
|
+
import parseDuration from "parse-duration";
|
|
21
|
+
import { stringify as uuidStringify } from "uuid";
|
|
22
|
+
import { z } from "zod/v4";
|
|
23
|
+
|
|
24
|
+
import { POWER_LEVELS } from "../ClientManager.ts";
|
|
25
|
+
import { JWT_EXPIRY } from "../Spire.ts";
|
|
26
|
+
import { getJwtSecret } from "../utils/jwtSecret.ts";
|
|
27
|
+
import { msgpack } from "../utils/msgpack.ts";
|
|
28
|
+
|
|
29
|
+
import { getAvatarRouter } from "./avatar.ts";
|
|
30
|
+
import { errorHandler } from "./errors.ts";
|
|
31
|
+
import { getFileRouter } from "./file.ts";
|
|
32
|
+
import { getInviteRouter } from "./invite.ts";
|
|
33
|
+
import { setupDocs } from "./openapi.ts";
|
|
34
|
+
import {
|
|
35
|
+
hasAnyPermission,
|
|
36
|
+
hasPermission,
|
|
37
|
+
userHasPermission,
|
|
38
|
+
} from "./permissions.ts";
|
|
39
|
+
import { globalLimiter } from "./rateLimit.ts";
|
|
40
|
+
import { getUserRouter } from "./user.ts";
|
|
41
|
+
import { censorUser, getParam, getUser } from "./utils.ts";
|
|
42
|
+
|
|
43
|
+
// expiry of regkeys
|
|
44
|
+
export const EXPIRY_TIME = 1000 * 60 * 5;
|
|
45
|
+
|
|
46
|
+
export const ALLOWED_IMAGE_TYPES = [
|
|
47
|
+
"image/jpeg",
|
|
48
|
+
"image/png",
|
|
49
|
+
"image/gif",
|
|
50
|
+
"image/apng",
|
|
51
|
+
"image/avif",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// ── Zod schemas for trust-boundary validation ──────────────────────────
|
|
55
|
+
const invitePayload = z.object({
|
|
56
|
+
duration: z.string().min(1),
|
|
57
|
+
serverID: z.string().min(1),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const channelPayload = z.object({
|
|
61
|
+
name: z.string().min(1).max(255),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const deviceListPayload = z.array(z.string());
|
|
65
|
+
|
|
66
|
+
const connectPayload = z.object({
|
|
67
|
+
signed: z.custom<Uint8Array>((val) => val instanceof Uint8Array),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const safePathParam = z.string().regex(/^[a-zA-Z0-9._-]+$/);
|
|
71
|
+
|
|
72
|
+
const emojiPayload = z.object({
|
|
73
|
+
file: z.string().optional(),
|
|
74
|
+
name: z.string().min(1),
|
|
75
|
+
signed: z.string().optional(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const jwtUserPayload = z.object({
|
|
79
|
+
exp: z.number().optional(),
|
|
80
|
+
user: UserSchema,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const jwtDevicePayload = z.object({
|
|
84
|
+
device: z.object({
|
|
85
|
+
deleted: z.boolean(),
|
|
86
|
+
deviceID: z.string(),
|
|
87
|
+
lastLogin: z.string(),
|
|
88
|
+
name: z.string(),
|
|
89
|
+
owner: z.string(),
|
|
90
|
+
signKey: z.string(),
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** Extract Bearer token from Authorization header. */
|
|
95
|
+
function extractBearer(req: express.Request): null | string {
|
|
96
|
+
const header = req.headers.authorization;
|
|
97
|
+
if (!header || !header.startsWith("Bearer ")) return null;
|
|
98
|
+
return header.slice(7);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const checkAuth: express.RequestHandler = (req, _res, next) => {
|
|
102
|
+
const token = extractBearer(req);
|
|
103
|
+
if (token) {
|
|
104
|
+
try {
|
|
105
|
+
const result = jwt.verify(token, getJwtSecret());
|
|
106
|
+
const parsed = jwtUserPayload.safeParse(result);
|
|
107
|
+
if (parsed.success) {
|
|
108
|
+
req.user = parsed.data.user;
|
|
109
|
+
if (parsed.data.exp !== undefined) {
|
|
110
|
+
req.exp = parsed.data.exp;
|
|
111
|
+
}
|
|
112
|
+
req.bearerToken = token;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Token verification failed — continue without auth
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
next();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const checkDevice: express.RequestHandler = (req, _res, next) => {
|
|
122
|
+
const token = req.headers["x-device-token"];
|
|
123
|
+
if (typeof token === "string" && token) {
|
|
124
|
+
try {
|
|
125
|
+
const result = jwt.verify(token, getJwtSecret());
|
|
126
|
+
const parsed = jwtDevicePayload.safeParse(result);
|
|
127
|
+
if (parsed.success) {
|
|
128
|
+
req.device = parsed.data.device;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Device token verification failed — continue without device
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
next();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const protect: express.RequestHandler = (req, res, next) => {
|
|
138
|
+
if (!req.user) {
|
|
139
|
+
res.sendStatus(401);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
next();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const msgpackParser: express.RequestHandler = (req, res, next) => {
|
|
147
|
+
if (req.is("application/msgpack")) {
|
|
148
|
+
try {
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument -- Express req.body is any; decoded body is validated by route-level Zod schemas
|
|
150
|
+
req.body = msgpack.decode(req.body);
|
|
151
|
+
} catch {
|
|
152
|
+
res.sendStatus(400);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
next();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const isProduction = process.env["NODE_ENV"] === "production";
|
|
160
|
+
|
|
161
|
+
const directories = ["files", "avatars"];
|
|
162
|
+
for (const dir of directories) {
|
|
163
|
+
if (!fs.existsSync(dir)) {
|
|
164
|
+
fs.mkdirSync(dir);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const initApp = (
|
|
169
|
+
api: express.Application,
|
|
170
|
+
db: Database,
|
|
171
|
+
log: winston.Logger,
|
|
172
|
+
tokenValidator: (key: string, scope: TokenScopes) => boolean,
|
|
173
|
+
signKeys: KeyPair,
|
|
174
|
+
notify: (
|
|
175
|
+
userID: string,
|
|
176
|
+
event: string,
|
|
177
|
+
transmissionID: string,
|
|
178
|
+
data?: unknown,
|
|
179
|
+
deviceID?: string,
|
|
180
|
+
) => void,
|
|
181
|
+
) => {
|
|
182
|
+
// INIT ROUTERS
|
|
183
|
+
const userRouter = getUserRouter(db, log, tokenValidator);
|
|
184
|
+
const fileRouter = getFileRouter(db, log);
|
|
185
|
+
const avatarRouter = getAvatarRouter(db, log);
|
|
186
|
+
const inviteRouter = getInviteRouter(db, log, tokenValidator, notify);
|
|
187
|
+
|
|
188
|
+
// MIDDLEWARE
|
|
189
|
+
// Global per-IP rate limit is the FIRST middleware so a flooded
|
|
190
|
+
// source hits the limiter before Express spends any cycles on
|
|
191
|
+
// body parsing, helmet, or auth. See src/server/rateLimit.ts.
|
|
192
|
+
api.use(globalLimiter);
|
|
193
|
+
api.use(express.json({ limit: "20mb" }));
|
|
194
|
+
api.use(
|
|
195
|
+
express.raw({
|
|
196
|
+
limit: "20mb",
|
|
197
|
+
type: "application/msgpack",
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
if (isProduction) {
|
|
201
|
+
api.use(helmet());
|
|
202
|
+
}
|
|
203
|
+
api.use(msgpackParser);
|
|
204
|
+
api.use(checkAuth);
|
|
205
|
+
api.use(checkDevice);
|
|
206
|
+
|
|
207
|
+
if (!jestRun()) {
|
|
208
|
+
api.use(morgan("dev", { stream: process.stdout }));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
api.use(cors({ credentials: true }));
|
|
212
|
+
|
|
213
|
+
api.get("/server/:id", protect, async (req, res) => {
|
|
214
|
+
const server = await db.retrieveServer(getParam(req, "id"));
|
|
215
|
+
|
|
216
|
+
if (server) {
|
|
217
|
+
return res.send(msgpack.encode(server));
|
|
218
|
+
} else {
|
|
219
|
+
return res.sendStatus(404);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
api.post("/server/:name", protect, async (req, res) => {
|
|
224
|
+
const userDetails = getUser(req);
|
|
225
|
+
const serverName = atob(getParam(req, "name"));
|
|
226
|
+
|
|
227
|
+
const server = await db.createServer(serverName, userDetails.userID);
|
|
228
|
+
res.send(msgpack.encode(server));
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
api.post("/server/:serverID/invites", protect, async (req, res) => {
|
|
232
|
+
const userDetails = getUser(req);
|
|
233
|
+
|
|
234
|
+
const parsedPayload = invitePayload.safeParse(req.body);
|
|
235
|
+
if (!parsedPayload.success) {
|
|
236
|
+
res.status(400).json({
|
|
237
|
+
error: "Invalid invite payload",
|
|
238
|
+
issues: parsedPayload.error.issues,
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const payload = parsedPayload.data;
|
|
243
|
+
const serverEntry = await db.retrieveServer(getParam(req, "serverID"));
|
|
244
|
+
|
|
245
|
+
if (!serverEntry) {
|
|
246
|
+
res.sendStatus(404);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const permissions = await db.retrievePermissions(
|
|
251
|
+
userDetails.userID,
|
|
252
|
+
"server",
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
!hasPermission(
|
|
257
|
+
permissions,
|
|
258
|
+
getParam(req, "serverID"),
|
|
259
|
+
POWER_LEVELS.INVITE,
|
|
260
|
+
)
|
|
261
|
+
) {
|
|
262
|
+
log.warn("No permission!");
|
|
263
|
+
res.sendStatus(401);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const duration = parseDuration(payload.duration, "ms");
|
|
268
|
+
|
|
269
|
+
if (!duration) {
|
|
270
|
+
res.sendStatus(400);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const expires = new Date(Date.now() + duration);
|
|
275
|
+
|
|
276
|
+
const invite = await db.createInvite(
|
|
277
|
+
crypto.randomUUID(),
|
|
278
|
+
serverEntry.serverID,
|
|
279
|
+
userDetails.userID,
|
|
280
|
+
expires.toString(),
|
|
281
|
+
);
|
|
282
|
+
res.send(msgpack.encode(invite));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
api.get("/server/:serverID/invites", protect, async (req, res) => {
|
|
286
|
+
const userDetails = getUser(req);
|
|
287
|
+
|
|
288
|
+
const permissions = await db.retrievePermissions(
|
|
289
|
+
userDetails.userID,
|
|
290
|
+
"server",
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (
|
|
294
|
+
!hasPermission(
|
|
295
|
+
permissions,
|
|
296
|
+
getParam(req, "serverID"),
|
|
297
|
+
POWER_LEVELS.INVITE,
|
|
298
|
+
)
|
|
299
|
+
) {
|
|
300
|
+
res.sendStatus(401);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const inviteList = await db.retrieveServerInvites(
|
|
305
|
+
getParam(req, "serverID"),
|
|
306
|
+
);
|
|
307
|
+
res.send(msgpack.encode(inviteList));
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
api.delete("/server/:id", protect, async (req, res) => {
|
|
311
|
+
const userDetails = getUser(req);
|
|
312
|
+
const serverID = getParam(req, "id");
|
|
313
|
+
const permissions = await db.retrievePermissions(
|
|
314
|
+
userDetails.userID,
|
|
315
|
+
"server",
|
|
316
|
+
);
|
|
317
|
+
if (hasPermission(permissions, serverID, POWER_LEVELS.DELETE)) {
|
|
318
|
+
await db.deleteServer(serverID);
|
|
319
|
+
res.sendStatus(200);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
res.sendStatus(401);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
api.post("/server/:id/channels", protect, async (req, res) => {
|
|
326
|
+
const userDetails = getUser(req);
|
|
327
|
+
const serverID = getParam(req, "id");
|
|
328
|
+
// resourceID is serverID
|
|
329
|
+
const parsedBody = channelPayload.safeParse(req.body);
|
|
330
|
+
if (!parsedBody.success) {
|
|
331
|
+
res.status(400).json({
|
|
332
|
+
error: "Invalid channel payload",
|
|
333
|
+
issues: parsedBody.error.issues,
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const { name } = parsedBody.data;
|
|
338
|
+
const permissions = await db.retrievePermissions(
|
|
339
|
+
userDetails.userID,
|
|
340
|
+
"server",
|
|
341
|
+
);
|
|
342
|
+
if (hasPermission(permissions, serverID, POWER_LEVELS.CREATE)) {
|
|
343
|
+
const channel = await db.createChannel(name, serverID);
|
|
344
|
+
res.send(msgpack.encode(channel));
|
|
345
|
+
|
|
346
|
+
const affectedUsers = await db.retrieveAffectedUsers(serverID);
|
|
347
|
+
// tell everyone about server change
|
|
348
|
+
for (const user of affectedUsers) {
|
|
349
|
+
notify(
|
|
350
|
+
user.userID,
|
|
351
|
+
"serverChange",
|
|
352
|
+
crypto.randomUUID(),
|
|
353
|
+
serverID,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
res.sendStatus(401);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
api.get("/server/:id/channels", protect, async (req, res) => {
|
|
362
|
+
const serverID = getParam(req, "id");
|
|
363
|
+
const userDetails = getUser(req);
|
|
364
|
+
const permissions = await db.retrievePermissions(
|
|
365
|
+
userDetails.userID,
|
|
366
|
+
"server",
|
|
367
|
+
);
|
|
368
|
+
if (hasAnyPermission(permissions, serverID)) {
|
|
369
|
+
const channels = await db.retrieveChannels(serverID);
|
|
370
|
+
res.send(msgpack.encode(channels));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
res.sendStatus(401);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
api.get("/server/:serverID/emoji", protect, async (req, res) => {
|
|
377
|
+
const rows = await db.retrieveEmojiList(getParam(req, "serverID"));
|
|
378
|
+
res.send(msgpack.encode(rows));
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
api.get("/server/:serverID/permissions", protect, async (req, res) => {
|
|
382
|
+
const userDetails = getUser(req);
|
|
383
|
+
const serverID = getParam(req, "serverID");
|
|
384
|
+
const permissions = await db.retrievePermissionsByResourceID(serverID);
|
|
385
|
+
const canSee = permissions.some(
|
|
386
|
+
(perm) => perm.userID === userDetails.userID,
|
|
387
|
+
);
|
|
388
|
+
if (!canSee) {
|
|
389
|
+
res.sendStatus(401);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
res.send(msgpack.encode(permissions));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
api.delete("/channel/:id", protect, async (req, res) => {
|
|
396
|
+
const channelID = getParam(req, "id");
|
|
397
|
+
const userDetails = getUser(req);
|
|
398
|
+
|
|
399
|
+
const channel = await db.retrieveChannel(channelID);
|
|
400
|
+
|
|
401
|
+
if (!channel) {
|
|
402
|
+
res.sendStatus(401);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const permissions = await db.retrievePermissions(
|
|
407
|
+
userDetails.userID,
|
|
408
|
+
"server",
|
|
409
|
+
);
|
|
410
|
+
for (const permission of permissions) {
|
|
411
|
+
if (
|
|
412
|
+
permission.resourceID === channel.serverID &&
|
|
413
|
+
permission.powerLevel > 50
|
|
414
|
+
) {
|
|
415
|
+
await db.deleteChannel(channelID);
|
|
416
|
+
|
|
417
|
+
res.sendStatus(200);
|
|
418
|
+
|
|
419
|
+
const affectedUsers = await db.retrieveAffectedUsers(
|
|
420
|
+
channel.serverID,
|
|
421
|
+
);
|
|
422
|
+
// tell everyone about server change
|
|
423
|
+
for (const user of affectedUsers) {
|
|
424
|
+
notify(
|
|
425
|
+
user.userID,
|
|
426
|
+
"serverChange",
|
|
427
|
+
crypto.randomUUID(),
|
|
428
|
+
channel.serverID,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
res.sendStatus(401);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
api.get("/channel/:id", protect, async (req, res) => {
|
|
438
|
+
const channel = await db.retrieveChannel(getParam(req, "id"));
|
|
439
|
+
|
|
440
|
+
if (channel) {
|
|
441
|
+
return res.send(msgpack.encode(channel));
|
|
442
|
+
} else {
|
|
443
|
+
return res.sendStatus(404);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
api.delete("/permission/:permissionID", protect, async (req, res) => {
|
|
448
|
+
const permissionID = getParam(req, "permissionID");
|
|
449
|
+
const userDetails = getUser(req);
|
|
450
|
+
const permToDelete = await db.retrievePermission(permissionID);
|
|
451
|
+
if (!permToDelete) {
|
|
452
|
+
res.sendStatus(404);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const permissions = await db.retrievePermissions(
|
|
457
|
+
userDetails.userID,
|
|
458
|
+
permToDelete.resourceType,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
for (const perm of permissions) {
|
|
462
|
+
if (
|
|
463
|
+
perm.resourceID === permToDelete.resourceID &&
|
|
464
|
+
(perm.userID === userDetails.userID ||
|
|
465
|
+
(perm.powerLevel > POWER_LEVELS.DELETE &&
|
|
466
|
+
perm.powerLevel > permToDelete.powerLevel))
|
|
467
|
+
) {
|
|
468
|
+
await db.deletePermission(permToDelete.permissionID);
|
|
469
|
+
res.sendStatus(200);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
res.sendStatus(401);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
api.post("/userList/:channelID", protect, async (req, res) => {
|
|
477
|
+
const userDetails = getUser(req);
|
|
478
|
+
const channelID = getParam(req, "channelID");
|
|
479
|
+
|
|
480
|
+
const channel = await db.retrieveChannel(channelID);
|
|
481
|
+
if (!channel) {
|
|
482
|
+
res.sendStatus(404);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const permissions = await db.retrievePermissions(
|
|
486
|
+
userDetails.userID,
|
|
487
|
+
"server",
|
|
488
|
+
);
|
|
489
|
+
for (const permission of permissions) {
|
|
490
|
+
if (permission.resourceID === channel.serverID) {
|
|
491
|
+
const groupMembers = await db.retrieveGroupMembers(channelID);
|
|
492
|
+
res.send(
|
|
493
|
+
msgpack.encode(
|
|
494
|
+
groupMembers.map((user) => censorUser(user)),
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
res.sendStatus(401);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
api.post("/deviceList", protect, async (req, res) => {
|
|
504
|
+
const parsed = deviceListPayload.safeParse(req.body);
|
|
505
|
+
if (!parsed.success) {
|
|
506
|
+
res.status(400).json({
|
|
507
|
+
error: "Expected array of user ID strings",
|
|
508
|
+
issues: parsed.error.issues,
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const devices = await db.retrieveUserDeviceList(parsed.data);
|
|
513
|
+
res.send(msgpack.encode(devices));
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
api.get("/device/:id", protect, async (req, res) => {
|
|
517
|
+
const device = await db.retrieveDevice(getParam(req, "id"));
|
|
518
|
+
|
|
519
|
+
if (device) {
|
|
520
|
+
return res.send(msgpack.encode(device));
|
|
521
|
+
} else {
|
|
522
|
+
return res.sendStatus(404);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
api.post("/device/:id/keyBundle", protect, async (req, res) => {
|
|
527
|
+
try {
|
|
528
|
+
const keyBundle = await db.getKeyBundle(getParam(req, "id"));
|
|
529
|
+
if (keyBundle) {
|
|
530
|
+
res.send(msgpack.encode(keyBundle));
|
|
531
|
+
} else {
|
|
532
|
+
res.sendStatus(404);
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
res.sendStatus(500);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
api.post("/device/:id/mail", protect, async (req, res) => {
|
|
540
|
+
const deviceDetails = req.device;
|
|
541
|
+
if (!deviceDetails) {
|
|
542
|
+
res.sendStatus(401);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const inbox = await db.retrieveMail(deviceDetails.deviceID);
|
|
546
|
+
res.send(msgpack.encode(inbox));
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
api.post("/device/:id/connect", protect, async (req, res) => {
|
|
550
|
+
const parsedBody = connectPayload.safeParse(req.body);
|
|
551
|
+
if (!parsedBody.success) {
|
|
552
|
+
res.status(400).json({
|
|
553
|
+
error: "Invalid connect payload",
|
|
554
|
+
issues: parsedBody.error.issues,
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const { signed } = parsedBody.data;
|
|
559
|
+
const device = await db.retrieveDevice(getParam(req, "id"));
|
|
560
|
+
if (!device) {
|
|
561
|
+
res.sendStatus(404);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const regKey = xSignOpen(signed, XUtils.decodeHex(device.signKey));
|
|
566
|
+
if (
|
|
567
|
+
regKey &&
|
|
568
|
+
tokenValidator(uuidStringify(regKey), TokenScopes.Connect)
|
|
569
|
+
) {
|
|
570
|
+
const token = jwt.sign({ device }, getJwtSecret(), {
|
|
571
|
+
expiresIn: JWT_EXPIRY,
|
|
572
|
+
});
|
|
573
|
+
jwt.verify(token, getJwtSecret());
|
|
574
|
+
|
|
575
|
+
res.send(msgpack.encode({ deviceToken: token }));
|
|
576
|
+
} else {
|
|
577
|
+
res.sendStatus(401);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
api.get("/device/:id/otk/count", protect, async (req, res) => {
|
|
582
|
+
const deviceDetails = req.device;
|
|
583
|
+
if (!deviceDetails) {
|
|
584
|
+
res.sendStatus(401);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const count = await db.getOTKCount(deviceDetails.deviceID);
|
|
588
|
+
res.send(msgpack.encode({ count }));
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
api.post("/device/:id/otk", protect, async (req, res) => {
|
|
592
|
+
const parsedOTKs = z.array(PreKeysWSSchema).safeParse(req.body);
|
|
593
|
+
if (!parsedOTKs.success) {
|
|
594
|
+
res.status(400).json({
|
|
595
|
+
error: "Invalid OTK payload",
|
|
596
|
+
issues: parsedOTKs.error.issues,
|
|
597
|
+
});
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const submittedOTKs = parsedOTKs.data;
|
|
601
|
+
if (submittedOTKs.length === 0) {
|
|
602
|
+
res.sendStatus(200);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const userDetails = getUser(req);
|
|
607
|
+
|
|
608
|
+
const deviceID = getParam(req, "id");
|
|
609
|
+
const otk = submittedOTKs[0];
|
|
610
|
+
|
|
611
|
+
const device = await db.retrieveDevice(deviceID);
|
|
612
|
+
if (!device || !otk) {
|
|
613
|
+
res.sendStatus(404);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const message = xSignOpen(
|
|
618
|
+
otk.signature,
|
|
619
|
+
XUtils.decodeHex(device.signKey),
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
if (!message) {
|
|
623
|
+
res.sendStatus(401);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
await db.saveOTK(userDetails.userID, deviceID, submittedOTKs);
|
|
628
|
+
res.sendStatus(200);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
api.get("/emoji/:emojiID/details", protect, async (req, res) => {
|
|
632
|
+
const emoji = await db.retrieveEmoji(getParam(req, "emojiID"));
|
|
633
|
+
res.send(msgpack.encode(emoji));
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
api.get("/emoji/:emojiID", protect, async (req, res) => {
|
|
637
|
+
const safeId = safePathParam.safeParse(getParam(req, "emojiID"));
|
|
638
|
+
if (!safeId.success) {
|
|
639
|
+
res.sendStatus(400);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const filePath = "./emoji/" + safeId.data;
|
|
643
|
+
const typeDetails = await fileTypeFromFile(filePath).catch(() => null);
|
|
644
|
+
if (!typeDetails) {
|
|
645
|
+
res.sendStatus(404);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
res.set("Content-type", typeDetails.mime);
|
|
649
|
+
res.set("Cache-control", "public, max-age=31536000");
|
|
650
|
+
|
|
651
|
+
const stream = fs.createReadStream(filePath);
|
|
652
|
+
stream.on("error", (err) => {
|
|
653
|
+
log.error(err.toString());
|
|
654
|
+
res.sendStatus(500);
|
|
655
|
+
});
|
|
656
|
+
stream.pipe(res);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
api.post("/emoji/:serverID/json", protect, async (req, res) => {
|
|
660
|
+
const parsedPayload = emojiPayload.safeParse(req.body);
|
|
661
|
+
if (!parsedPayload.success) {
|
|
662
|
+
res.status(400).json({
|
|
663
|
+
error: "Invalid emoji payload",
|
|
664
|
+
issues: parsedPayload.error.issues,
|
|
665
|
+
});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const payload = parsedPayload.data;
|
|
669
|
+
|
|
670
|
+
const userDetails = getUser(req);
|
|
671
|
+
const device = req.device;
|
|
672
|
+
|
|
673
|
+
if (!device) {
|
|
674
|
+
res.sendStatus(401);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!payload.file) {
|
|
679
|
+
res.sendStatus(400);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const buf = Buffer.from(XUtils.decodeBase64(payload.file));
|
|
684
|
+
const serverEntry = await db.retrieveServer(getParam(req, "serverID"));
|
|
685
|
+
|
|
686
|
+
const permissionList = await db.retrievePermissionsByResourceID(
|
|
687
|
+
getParam(req, "serverID"),
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
if (
|
|
691
|
+
!userHasPermission(
|
|
692
|
+
permissionList,
|
|
693
|
+
userDetails.userID,
|
|
694
|
+
POWER_LEVELS.EMOJI,
|
|
695
|
+
)
|
|
696
|
+
) {
|
|
697
|
+
res.sendStatus(401);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (!serverEntry) {
|
|
701
|
+
res.sendStatus(404);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (!payload.name) {
|
|
705
|
+
res.sendStatus(400);
|
|
706
|
+
}
|
|
707
|
+
if (Buffer.byteLength(buf) > 256000) {
|
|
708
|
+
log.warn("File too big.");
|
|
709
|
+
res.sendStatus(413);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const mimeType = await fileTypeFromBuffer(buf);
|
|
713
|
+
if (!ALLOWED_IMAGE_TYPES.includes(mimeType?.mime || "no/type")) {
|
|
714
|
+
res.status(400).send({
|
|
715
|
+
error:
|
|
716
|
+
"Unsupported file type. Expected jpeg, png, gif, apng, or avif but received " +
|
|
717
|
+
String(mimeType?.ext),
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const emoji: Emoji = {
|
|
723
|
+
emojiID: crypto.randomUUID(),
|
|
724
|
+
name: payload.name,
|
|
725
|
+
owner: getParam(req, "serverID"),
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
await db.createEmoji(emoji);
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
// write the file to disk
|
|
732
|
+
await fsp.writeFile("emoji/" + emoji.emojiID, buf);
|
|
733
|
+
log.info("Wrote new emoji " + emoji.emojiID);
|
|
734
|
+
res.send(msgpack.encode(emoji));
|
|
735
|
+
} catch (err: unknown) {
|
|
736
|
+
log.warn(String(err));
|
|
737
|
+
res.sendStatus(500);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
api.post(
|
|
742
|
+
"/emoji/:serverID",
|
|
743
|
+
protect,
|
|
744
|
+
multer().single("emoji"),
|
|
745
|
+
async (req, res) => {
|
|
746
|
+
const parsedPayload = emojiPayload.safeParse(req.body);
|
|
747
|
+
if (!parsedPayload.success) {
|
|
748
|
+
res.status(400).json({
|
|
749
|
+
error: "Invalid emoji payload",
|
|
750
|
+
issues: parsedPayload.error.issues,
|
|
751
|
+
});
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const payload = parsedPayload.data;
|
|
755
|
+
const serverID = getParam(req, "serverID");
|
|
756
|
+
if (typeof serverID !== "string") {
|
|
757
|
+
res.sendStatus(400);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const serverEntry = await db.retrieveServer(serverID);
|
|
761
|
+
const userDetails = getUser(req);
|
|
762
|
+
const deviceDetails = req.device;
|
|
763
|
+
if (!deviceDetails) {
|
|
764
|
+
res.sendStatus(401);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const permissionList =
|
|
769
|
+
await db.retrievePermissionsByResourceID(serverID);
|
|
770
|
+
|
|
771
|
+
if (
|
|
772
|
+
!userHasPermission(
|
|
773
|
+
permissionList,
|
|
774
|
+
userDetails.userID,
|
|
775
|
+
POWER_LEVELS.EMOJI,
|
|
776
|
+
)
|
|
777
|
+
) {
|
|
778
|
+
res.sendStatus(401);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (!serverEntry) {
|
|
783
|
+
res.sendStatus(404);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (!payload.name) {
|
|
788
|
+
res.sendStatus(400);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (!req.file) {
|
|
792
|
+
log.warn("MISSING FILE");
|
|
793
|
+
res.sendStatus(400);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (Buffer.byteLength(req.file.buffer) > 256000) {
|
|
798
|
+
log.warn("File too big.");
|
|
799
|
+
res.sendStatus(413);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const mimeType = await fileTypeFromBuffer(req.file.buffer);
|
|
803
|
+
if (!ALLOWED_IMAGE_TYPES.includes(mimeType?.mime || "no/type")) {
|
|
804
|
+
res.status(400).send({
|
|
805
|
+
error:
|
|
806
|
+
"Unsupported file type. Expected jpeg, png, gif, apng, or avif but received " +
|
|
807
|
+
String(mimeType?.ext),
|
|
808
|
+
});
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const emoji: Emoji = {
|
|
813
|
+
emojiID: crypto.randomUUID(),
|
|
814
|
+
name: payload.name,
|
|
815
|
+
owner: serverID,
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
await db.createEmoji(emoji);
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
// write the file to disk
|
|
822
|
+
await fsp.writeFile("emoji/" + emoji.emojiID, req.file.buffer);
|
|
823
|
+
log.info("Wrote new emoji " + emoji.emojiID);
|
|
824
|
+
res.send(msgpack.encode(emoji));
|
|
825
|
+
} catch (err: unknown) {
|
|
826
|
+
log.warn(String(err));
|
|
827
|
+
res.sendStatus(500);
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
// COMPLEX RESOURCES
|
|
833
|
+
api.use("/user", userRouter);
|
|
834
|
+
|
|
835
|
+
api.use("/file", fileRouter);
|
|
836
|
+
|
|
837
|
+
api.use("/avatar", avatarRouter);
|
|
838
|
+
|
|
839
|
+
api.use("/invite", inviteRouter);
|
|
840
|
+
|
|
841
|
+
setupDocs(api);
|
|
842
|
+
|
|
843
|
+
// Central error handler MUST be last. Handles both thrown AppErrors
|
|
844
|
+
// (client-safe status + message) and programmer errors (generic 500
|
|
845
|
+
// with full details logged server-side). See src/server/errors.ts
|
|
846
|
+
// for the CWE mapping.
|
|
847
|
+
api.use(errorHandler(log));
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* @ignore
|
|
852
|
+
*/
|
|
853
|
+
const jestRun = () => {
|
|
854
|
+
return process.env["JEST_WORKER_ID"] !== undefined;
|
|
855
|
+
};
|