diff --git a/src/Server.ts b/src/Server.ts
index aa66b5b6..fcc5374b 100644
--- a/src/Server.ts
+++ b/src/Server.ts
@@ -10,10 +10,10 @@ import i18nextBackend from "i18next-node-fs-backend";
import { ErrorHandler } from "./middlewares/ErrorHandler";
import { BodyParser } from "./middlewares/BodyParser";
import express, { Router, Request, Response } from "express";
-import fetch, { Response as FetchResponse } from "node-fetch";
import mongoose from "mongoose";
import path from "path";
import RateLimit from "./middlewares/RateLimit";
+import TestClient from "./middlewares/TestClient";
// this will return the new updated document for findOneAndUpdate
mongoose.set("returnOriginal", false); // https://mongoosejs.com/docs/api/model.html#model_Model.findOneAndUpdate
@@ -29,14 +29,6 @@ declare global {
}
}
-const assetCache = new Map<
- string,
- {
- response: FetchResponse;
- buffer: Buffer;
- }
->();
-
export class FosscordServer extends Server {
public declare options: FosscordServerOptions;
@@ -69,7 +61,7 @@ export class FosscordServer extends Server {
this.app.use(CORS);
this.app.use(Authentication);
- this.app.use(BodyParser({ inflate: true, limit: 1024 * 1024 * 2 }));
+ this.app.use(BodyParser({ inflate: true, limit: 1024 * 1024 * 10 })); // 2MB
const languages = fs.readdirSync(path.join(__dirname, "..", "locales"));
const namespaces = fs.readdirSync(path.join(__dirname, "..", "locales", "en"));
const ns = namespaces.filter((x) => x.endsWith(".json")).map((x) => x.slice(0, x.length - 5));
@@ -90,21 +82,21 @@ export class FosscordServer extends Server {
this.app.use(i18nextMiddleware.handle(i18next, {}));
const app = this.app;
- const prefix = Router();
+ const api = Router();
// @ts-ignore
- this.app = prefix;
- prefix.use(RateLimit({ bucket: "global", count: 10, window: 5, bot: 250 }));
- prefix.use(RateLimit({ bucket: "error", count: 5, error: true, window: 5, bot: 15, onylIp: true }));
- prefix.use("/guilds/:id", RateLimit({ count: 5, window: 5 }));
- prefix.use("/webhooks/:id", RateLimit({ count: 5, window: 5 }));
- prefix.use("/channels/:id", RateLimit({ count: 5, window: 5 }));
+ this.app = api;
+ api.use(RateLimit({ bucket: "global", count: 10, window: 5, bot: 250 }));
+ api.use(RateLimit({ bucket: "error", count: 5, error: true, window: 5, bot: 15, onylIp: true }));
+ api.use("/guilds/:id", RateLimit({ count: 5, window: 5 }));
+ api.use("/webhooks/:id", RateLimit({ count: 5, window: 5 }));
+ api.use("/channels/:id", RateLimit({ count: 5, window: 5 }));
this.routes = await this.registerRoutes(path.join(__dirname, "routes", "/"));
- app.use("/api/v8", prefix);
- app.use("/api/v9", prefix);
- app.use("/api", prefix); // allow unversioned requests
+ app.use("/api/v8", api);
+ app.use("/api/v9", api);
+ app.use("/api", api); // allow unversioned requests
- prefix.get("*", (req: Request, res: Response) => {
+ api.get("*", (req: Request, res: Response) => {
res.status(404).json({
message: "404: Not Found",
code: 0
@@ -113,61 +105,8 @@ export class FosscordServer extends Server {
this.app = app;
this.app.use(ErrorHandler);
- const indexHTML = fs.readFileSync(path.join(__dirname, "..", "client_test", "index.html"), { encoding: "utf8" });
-
- this.app.use("/assets", express.static(path.join(__dirname, "..", "assets")));
-
- this.app.get("/assets/:file", async (req: Request, res: Response) => {
- delete req.headers.host;
- var response: FetchResponse;
- var buffer: Buffer;
- const cache = assetCache.get(req.params.file);
- if (!cache) {
- response = await fetch(`https://discord.com/assets/${req.params.file}`, {
- // @ts-ignore
- headers: {
- ...req.headers
- }
- });
- buffer = await response.buffer();
- } else {
- response = cache.response;
- buffer = cache.buffer;
- }
+ TestClient(this.app);
- response.headers.forEach((value, name) => {
- if (
- [
- "content-length",
- "content-security-policy",
- "strict-transport-security",
- "set-cookie",
- "transfer-encoding",
- "expect-ct",
- "access-control-allow-origin",
- "content-encoding"
- ].includes(name.toLowerCase())
- ) {
- return;
- }
- res.set(name, value);
- });
- assetCache.set(req.params.file, { buffer, response });
-
- return res.send(buffer);
- });
- this.app.get("*", (req: Request, res: Response) => {
- res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
- res.set("content-type", "text/html");
- res.send(
- indexHTML
- .replace(
- /CDN_HOST: ".+"/,
- `CDN_HOST: "${(Config.get().cdn.endpoint || "http://localhost:3003").replace(/https?:/, "")}"`
- )
- .replace(/GATEWAY_ENDPOINT: ".+"/, `GATEWAY_ENDPOINT: "${Config.get().gateway.endpoint || "ws://localhost:3002"}"`)
- );
- });
return super.start();
}
}
diff --git a/src/middlewares/Authentication.ts b/src/middlewares/Authentication.ts
index 76b335ad..01b7ef57 100644
--- a/src/middlewares/Authentication.ts
+++ b/src/middlewares/Authentication.ts
@@ -6,6 +6,7 @@ export const NO_AUTHORIZATION_ROUTES = [
/^\/api(\/v\d+)?\/auth\/login/,
/^\/api(\/v\d+)?\/auth\/register/,
/^\/api(\/v\d+)?\/webhooks\//,
+ /^\/api(\/v\d+)?\/ping/,
/^\/api(\/v\d+)?\/gateway/,
/^\/api(\/v\d+)?\/experiments/,
/^\/api(\/v\d+)?\/guilds\/\d+\/widget\.(json|png)/
diff --git a/src/middlewares/RateLimit.ts b/src/middlewares/RateLimit.ts
index 89e002df..088c3161 100644
--- a/src/middlewares/RateLimit.ts
+++ b/src/middlewares/RateLimit.ts
@@ -1,5 +1,5 @@
import { db, MongooseCache, Bucket } from "@fosscord/server-util";
-import { NextFunction, Request, Response } from "express";
+import { IRouterHandler, NextFunction, Request, Response } from "express";
import { getIpAdress } from "../util/ipAddress";
import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
@@ -43,10 +43,10 @@ export default function RateLimit(opts: {
error?: boolean;
success?: boolean;
onylIp?: boolean;
-}) {
+}): any {
Cache.init(); // will only initalize it once
- return async (req: Request, res: Response, next: NextFunction) => {
+ return async (req: Request, res: Response, next: NextFunction): Promise<any> => {
const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, "");
var user_id = getIpAdress(req);
if (!opts.onylIp && req.user_id) user_id = req.user_id;
diff --git a/src/middlewares/TestClient.ts b/src/middlewares/TestClient.ts
new file mode 100644
index 00000000..4e3c9de3
--- /dev/null
+++ b/src/middlewares/TestClient.ts
@@ -0,0 +1,66 @@
+import bodyParser, { OptionsJson } from "body-parser";
+import express, { NextFunction, Request, Response, Application } from "express";
+import { HTTPError } from "lambert-server";
+import fs from "fs";
+import path from "path";
+import fetch, { Response as FetchResponse } from "node-fetch";
+import { Config } from "@fosscord/server-util";
+
+export default function TestClient(app: Application) {
+ const assetCache = new Map<string, { response: FetchResponse; buffer: Buffer }>();
+ const indexHTML = fs.readFileSync(path.join(__dirname, "..", "..", "client_test", "index.html"), { encoding: "utf8" });
+
+ app.use("/assets", express.static(path.join(__dirname, "..", "assets")));
+
+ app.get("/assets/:file", async (req: Request, res: Response) => {
+ delete req.headers.host;
+ var response: FetchResponse;
+ var buffer: Buffer;
+ const cache = assetCache.get(req.params.file);
+ if (!cache) {
+ response = await fetch(`https://discord.com/assets/${req.params.file}`, {
+ // @ts-ignore
+ headers: {
+ ...req.headers
+ }
+ });
+ buffer = await response.buffer();
+ } else {
+ response = cache.response;
+ buffer = cache.buffer;
+ }
+
+ response.headers.forEach((value, name) => {
+ if (
+ [
+ "content-length",
+ "content-security-policy",
+ "strict-transport-security",
+ "set-cookie",
+ "transfer-encoding",
+ "expect-ct",
+ "access-control-allow-origin",
+ "content-encoding"
+ ].includes(name.toLowerCase())
+ ) {
+ return;
+ }
+ res.set(name, value);
+ });
+ assetCache.set(req.params.file, { buffer, response });
+
+ return res.send(buffer);
+ });
+ app.get("*", (req: Request, res: Response) => {
+ res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24);
+ res.set("content-type", "text/html");
+ var html = indexHTML;
+ const CDN_ENDPOINT = (Config.get()?.cdn.endpoint || process.env.CDN || "").replace(/(https?)?(:\/\/?)/g, "");
+ const GATEWAY_ENDPOINT = Config.get()?.gateway.endpoint || process.env.GATEWAY || "";
+
+ if (CDN_ENDPOINT) html = html.replace(/CDN_HOST: .+/, `CDN_HOST: "${CDN_ENDPOINT}",`);
+ if (GATEWAY_ENDPOINT) html = html.replace(/GATEWAY_ENDPOINT: .+/, `GATEWAY_ENDPOINT: "${GATEWAY_ENDPOINT}",`);
+
+ res.send(html);
+ });
+}
diff --git a/src/routes/auth/login.ts b/src/routes/auth/login.ts
index af00a46d..c92ddccc 100644
--- a/src/routes/auth/login.ts
+++ b/src/routes/auth/login.ts
@@ -9,7 +9,8 @@ import RateLimit from "../../middlewares/RateLimit";
const router: Router = Router();
export default router;
-// TODO: check if user is deleted/restricted
+// TODO: check if user is deleted --> prohibit login
+
router.post(
"/",
RateLimit({ count: 5, window: 60, onylIp: true }),
@@ -22,7 +23,7 @@ router.post(
$gift_code_sku_id: String
}),
async (req: Request, res: Response) => {
- const { login, password, captcha_key } = req.body;
+ const { login, password, captcha_key, undelete } = req.body;
const email = adjustEmail(login);
const query: any[] = [{ phone: login }];
if (email) query.push({ email });
@@ -62,6 +63,13 @@ router.post(
throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } });
});
+ if (user.disabled && undelete) {
+ // undelete refers to un'disable' here
+ await UserModel.updateOne({ id: req.user_id }, { disabled: false }).exec();
+ } else if (user.disabled) {
+ return res.status(400).json({ message: req.t("auth:login.ACCOUNT_DISABLED"), code: 20013 });
+ }
+
// the salt is saved in the password refer to bcrypt docs
const same_password = await bcrypt.compare(password, user.user_data.hash || "");
if (!same_password) {
@@ -100,14 +108,14 @@ export async function generateToken(id: string) {
/**
* POST /auth/login
* @argument { login: "email@gmail.com", password: "cleartextpassword", undelete: false, captcha_key: null, login_source: null, gift_code_sku_id: null, }
-
+
* MFA required:
* @returns {"token": null, "mfa": true, "sms": true, "ticket": "SOME TICKET JWT TOKEN"}
-
+
* Captcha required:
* @returns {"captcha_key": ["captcha-required"], "captcha_sitekey": null, "captcha_service": "recaptcha"}
-
+
* Sucess:
* @returns {"token": "USERTOKEN", "user_settings": {"locale": "en", "theme": "dark"}}
-
+
*/
diff --git a/src/routes/auth/register.ts b/src/routes/auth/register.ts
index 279103bc..eb5cd97d 100644
--- a/src/routes/auth/register.ts
+++ b/src/routes/auth/register.ts
@@ -197,12 +197,13 @@ router.post(
discriminator,
avatar: null,
accent_color: null,
+ banner: null,
bot: false,
system: false,
desktop: false,
mobile: false,
- premium: false,
- premium_type: 0,
+ premium: true,
+ premium_type: 2,
phone: null,
bio: "",
mfa_enabled: false,
diff --git a/src/routes/channels/#channel_id/messages/index.ts b/src/routes/channels/#channel_id/messages/index.ts
index 4e42d546..59494c7e 100644
--- a/src/routes/channels/#channel_id/messages/index.ts
+++ b/src/routes/channels/#channel_id/messages/index.ts
@@ -30,7 +30,13 @@ export function isTextChannel(type: ChannelType): boolean {
// get messages
router.get("/", async (req: Request, res: Response) => {
const channel_id = req.params.channel_id;
- const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, type: true, permission_overwrites: true }).exec();
+ const channel = await ChannelModel.findOne(
+ { id: channel_id },
+ { guild_id: true, type: true, permission_overwrites: true, recipient_ids: true, owner_id: true }
+ )
+ .lean() // lean is needed, because we don't want to populate .recipients that also auto deletes .recipient_ids
+ .exec();
+ if (!channel) throw new HTTPError("Channel not found", 404);
isTextChannel(channel.type);
@@ -46,6 +52,7 @@ router.get("/", async (req: Request, res: Response) => {
if (!limit) limit = 50;
var halfLimit = Math.floor(limit / 2);
+ // @ts-ignore
const permissions = await getPermission(req.user_id, channel.guild_id, channel_id, { channel });
permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
@@ -126,7 +133,16 @@ router.post("/", messageUpload.single("file"), async (req: Request, res: Respons
const embeds = [];
if (body.embed) embeds.push(body.embed);
- const data = await sendMessage({ ...body, type: 0, pinned: false, author_id: req.user_id, embeds, channel_id, attachments, edited_timestamp: null });
+ const data = await sendMessage({
+ ...body,
+ type: 0,
+ pinned: false,
+ author_id: req.user_id,
+ embeds,
+ channel_id,
+ attachments,
+ edited_timestamp: null
+ });
return res.send(data);
});
diff --git a/src/routes/guilds/#guild_id/bans.ts b/src/routes/guilds/#guild_id/bans.ts
index 4d9bad37..d9752f61 100644
--- a/src/routes/guilds/#guild_id/bans.ts
+++ b/src/routes/guilds/#guild_id/bans.ts
@@ -16,7 +16,7 @@ router.get("/", async (req: Request, res: Response) => {
const guild = await GuildModel.exists({ id: guild_id });
if (!guild) throw new HTTPError("Guild not found", 404);
- var bans = await BanModel.find({ guild_id: guild_id }, { user: true, reason: true }).exec();
+ var bans = await BanModel.find({ guild_id: guild_id }, { user_id: true, reason: true }).exec();
return res.json(toObject(bans));
});
diff --git a/src/routes/guilds/#guild_id/channels.ts b/src/routes/guilds/#guild_id/channels.ts
index 90b4473d..15cc7394 100644
--- a/src/routes/guilds/#guild_id/channels.ts
+++ b/src/routes/guilds/#guild_id/channels.ts
@@ -23,15 +23,18 @@ router.get("/", async (req: Request, res: Response) => {
res.json(toObject(channels));
});
+// TODO: check if channel type is permitted
+// TODO: check if parent_id exists
router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const body = req.body as ChannelModifySchema;
const channel = await createChannel({ ...body, guild_id }, req.user_id);
- res.json(channel);
+ res.json(toObject(channel));
});
+// TODO: check if parent_id exists
router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response) => {
const { guild_id } = req.params;
const body = req.body as ChannelModifySchema;
@@ -41,7 +44,7 @@ router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response
await emitEvent({ event: "CHANNEL_UPDATE", data: channel } as ChannelUpdateEvent);
- res.json(channel);
+ res.json(toObject(channel));
});
export default router;
diff --git a/src/routes/guilds/#guild_id/delete.ts b/src/routes/guilds/#guild_id/delete.ts
index c363db25..6cca289e 100644
--- a/src/routes/guilds/#guild_id/delete.ts
+++ b/src/routes/guilds/#guild_id/delete.ts
@@ -4,6 +4,7 @@ import {
GuildDeleteEvent,
GuildModel,
InviteModel,
+ MemberModel,
MessageModel,
RoleModel,
UserModel
@@ -30,13 +31,16 @@ router.post("/", async (req: Request, res: Response) => {
guild_id: guild_id
} as GuildDeleteEvent);
- await GuildModel.deleteOne({ id: guild_id }).exec();
- await UserModel.updateMany({ guilds: guild_id }, { $pull: { guilds: guild_id } }).exec();
- await RoleModel.deleteMany({ guild_id }).exec();
- await ChannelModel.deleteMany({ guild_id }).exec();
- await EmojiModel.deleteMany({ guild_id }).exec();
- await InviteModel.deleteMany({ guild_id }).exec();
- await MessageModel.deleteMany({ guild_id }).exec();
+ await Promise.all([
+ GuildModel.deleteOne({ id: guild_id }).exec(),
+ UserModel.updateMany({ guilds: guild_id }, { $pull: { guilds: guild_id } }).exec(),
+ RoleModel.deleteMany({ guild_id }).exec(),
+ ChannelModel.deleteMany({ guild_id }).exec(),
+ EmojiModel.deleteMany({ guild_id }).exec(),
+ InviteModel.deleteMany({ guild_id }).exec(),
+ MessageModel.deleteMany({ guild_id }).exec(),
+ MemberModel.deleteMany({ guild_id }).exec()
+ ]);
return res.sendStatus(204);
});
diff --git a/src/routes/guilds/#guild_id/index.ts b/src/routes/guilds/#guild_id/index.ts
index 3af49106..dc4ddb39 100644
--- a/src/routes/guilds/#guild_id/index.ts
+++ b/src/routes/guilds/#guild_id/index.ts
@@ -17,6 +17,7 @@ import { HTTPError } from "lambert-server";
import { GuildUpdateSchema } from "../../../schema/Guild";
import { emitEvent } from "../../../util/Event";
import { check } from "../../../util/instanceOf";
+import { handleFile } from "../../../util/cdn";
import "missing-native-js-functions";
const router = Router();
@@ -42,6 +43,10 @@ router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response)
const perms = await getPermission(req.user_id, guild_id);
perms.hasThrow("MANAGE_GUILD");
+ if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon);
+ if (body.banner) body.banner = await handleFile(`/banners/${guild_id}`, body.banner);
+ if (body.splash) body.splash = await handleFile(`/splashes/${guild_id}`, body.splash);
+
const guild = await GuildModel.findOneAndUpdate({ id: guild_id }, body)
.populate({ path: "joined_at", match: { id: req.user_id } })
.exec();
@@ -50,7 +55,7 @@ router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response)
emitEvent({ event: "GUILD_UPDATE", data: data, guild_id } as GuildUpdateEvent);
- return res.send(data);
+ return res.json(data);
});
export default router;
diff --git a/src/routes/guilds/#guild_id/vanity-url.ts b/src/routes/guilds/#guild_id/vanity-url.ts
index 30c98d19..323b2647 100644
--- a/src/routes/guilds/#guild_id/vanity-url.ts
+++ b/src/routes/guilds/#guild_id/vanity-url.ts
@@ -1,16 +1,45 @@
-import { GuildModel } from "@fosscord/server-util";
+import { getPermission, GuildModel, InviteModel, trimSpecial } from "@fosscord/server-util";
import { Router, Request, Response } from "express";
import { HTTPError } from "lambert-server";
+import { check, Length } from "../../../util/instanceOf";
+import { isMember } from "../../../util/Member";
const router = Router();
+const InviteRegex = /\W/g;
+
router.get("/", async (req: Request, res: Response) => {
const { guild_id } = req.params;
+ await isMember(req.user_id, guild_id);
const guild = await GuildModel.findOne({ id: guild_id }).exec();
if (!guild.vanity_url) throw new HTTPError("This guild has no vanity url", 204);
- return res.json({ vanity_url: guild.vanity_url });
+ return res.json({ code: guild.vanity_url.code });
+});
+
+// TODO: check if guild is elgible for vanity url
+router.patch("/", check({ code: new Length(String, 0, 20) }), async (req: Request, res: Response) => {
+ const { guild_id } = req.params;
+ var code = req.body.code.replace(InviteRegex);
+ if (!code) code = null;
+
+ const permission = await getPermission(req.user_id, guild_id);
+ permission.hasThrow("MANAGE_GUILD");
+
+ const alreadyExists = await Promise.all([
+ GuildModel.findOne({ "vanity_url.code": code })
+ .exec()
+ .catch(() => null),
+ InviteModel.findOne({ code: code })
+ .exec()
+ .catch(() => null)
+ ]);
+ if (alreadyExists.some((x) => x)) throw new HTTPError("Vanity url already exists", 400);
+
+ await GuildModel.updateOne({ id: guild_id }, { "vanity_url.code": code }).exec();
+
+ return res.json({ code: code });
});
export default router;
diff --git a/src/routes/ping.ts b/src/routes/ping.ts
new file mode 100644
index 00000000..38daf81e
--- /dev/null
+++ b/src/routes/ping.ts
@@ -0,0 +1,9 @@
+import { Router, Response, Request } from "express";
+
+const router = Router();
+
+router.get("/", (req: Request, res: Response) => {
+ res.send("pong");
+});
+
+export default router;
diff --git a/src/routes/users/#id/profile.ts b/src/routes/users/#id/profile.ts
index b86b0b90..4b4b9439 100644
--- a/src/routes/users/#id/profile.ts
+++ b/src/routes/users/#id/profile.ts
@@ -17,6 +17,7 @@ router.get("/", async (req: Request, res: Response) => {
public_flags: user.public_flags,
avatar: user.avatar,
accent_color: user.accent_color,
+ banner: user.banner,
bio: req.user_bot ? null : user.bio,
bot: user.bot,
}
diff --git a/src/routes/users/@me/disable.ts b/src/routes/users/@me/disable.ts
index b16ef783..0e5b734e 100644
--- a/src/routes/users/@me/disable.ts
+++ b/src/routes/users/@me/disable.ts
@@ -1,10 +1,20 @@
+import { UserModel } from "@fosscord/server-util";
import { Router, Response, Request } from "express";
+import bcrypt from "bcrypt";
const router = Router();
-router.post("/", (req: Request, res: Response) => {
- // TODO:
- res.sendStatus(204);
+router.post("/", async (req: Request, res: Response) => {
+ const user = await UserModel.findOne({ id: req.user_id }).exec(); //User object
+
+ let correctpass = await bcrypt.compare(req.body.password, user!.user_data.hash); //Not sure if user typed right password :/
+ if (correctpass) {
+ await UserModel.updateOne({ id: req.user_id }, { disabled: true }).exec();
+
+ res.sendStatus(204);
+ } else {
+ res.status(400).json({ message: "Password does not match", code: 50018 });
+ }
});
export default router;
diff --git a/src/routes/users/@me/index.ts b/src/routes/users/@me/index.ts
index 68196afe..f6b29958 100644
--- a/src/routes/users/@me/index.ts
+++ b/src/routes/users/@me/index.ts
@@ -1,10 +1,9 @@
import { Router, Request, Response } from "express";
import { UserModel, toObject, PublicUserProjection } from "@fosscord/server-util";
-import { HTTPError } from "lambert-server";
import { getPublicUser } from "../../../util/User";
import { UserModifySchema } from "../../../schema/User";
import { check } from "../../../util/instanceOf";
-import { uploadFile } from "../../../util/cdn";
+import { handleFile } from "../../../util/cdn";
const router: Router = Router();
@@ -15,18 +14,8 @@ router.get("/", async (req: Request, res: Response) => {
router.patch("/", check(UserModifySchema), async (req: Request, res: Response) => {
const body = req.body as UserModifySchema;
- if (body.avatar) {
- try {
- const mimetype = body.avatar.split(":")[1].split(";")[0];
- const buffer = Buffer.from(body.avatar.split(",")[1], "base64");
-
- // @ts-ignore
- const { id } = await uploadFile(`/avatars/${req.user_id}`, { buffer, mimetype, originalname: "avatar" });
- body.avatar = id;
- } catch (error) {
- throw new HTTPError("Invalid avatar");
- }
- }
+ if(body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string);
+ if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string);
const user = await UserModel.findOneAndUpdate({ id: req.user_id }, body, { projection: PublicUserProjection }).exec();
// TODO: dispatch user update event
diff --git a/src/routes/users/@me/profile.ts b/src/routes/users/@me/profile.ts
index 0d295d05..b67d1964 100644
--- a/src/routes/users/@me/profile.ts
+++ b/src/routes/users/@me/profile.ts
@@ -17,6 +17,7 @@ router.get("/", async (req: Request, res: Response) => {
public_flags: user.public_flags,
avatar: user.avatar,
accent_color: user.accent_color,
+ banner: user.banner,
bio: user.bio,
bot: user.bot,
}
diff --git a/src/routes/users/@me/relationships.ts b/src/routes/users/@me/relationships.ts
index b874ec9a..a8f03143 100644
--- a/src/routes/users/@me/relationships.ts
+++ b/src/routes/users/@me/relationships.ts
@@ -4,43 +4,50 @@ import {
PublicUserProjection,
toObject,
RelationshipType,
- RelationshipRemoveEvent
+ RelationshipRemoveEvent,
+ UserDocument
} from "@fosscord/server-util";
import { Router, Response, Request } from "express";
-import { check, HTTPError } from "lambert-server";
+import { HTTPError } from "lambert-server";
import { emitEvent } from "../../../util/Event";
+import { check, Length } from "../../../util/instanceOf";
const router = Router();
const userProjection = { "user_data.relationships": true, ...PublicUserProjection };
-router.put("/:id", check({ $type: Number }), async (req: Request, res: Response) => {
- const { id } = req.params;
+router.get("/", async (req: Request, res: Response) => {
+ const user = await UserModel.findOne({ id: req.user_id }, { user_data: { relationships: true } })
+ .populate({ path: "user_data.relationships.id", model: UserModel })
+ .exec();
+
+ return res.json(toObject(user.user_data.relationships));
+});
+
+async function addRelationship(req: Request, res: Response, friend: UserDocument, type: RelationshipType) {
+ const id = friend.id;
if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend");
- const body = req.body as { type?: number };
const user = await UserModel.findOne({ id: req.user_id }, userProjection).exec();
- if (!user) throw new HTTPError("Invalid token", 400);
+ const newUserRelationships = [...user.user_data.relationships];
+ const newFriendRelationships = [...friend.user_data.relationships];
- const friend = await UserModel.findOne({ id }, userProjection).exec();
- if (!friend) throw new HTTPError("User not found", 404);
+ var relationship = newUserRelationships.find((x) => x.id === id);
+ const friendRequest = newFriendRelationships.find((x) => x.id === req.user_id);
- var relationship = user.user_data.relationships.find((x) => x.id === id);
- const friendRequest = friend.user_data.relationships.find((x) => x.id === req.user_id);
-
- if (body.type === RelationshipType.blocked) {
+ if (type === RelationshipType.blocked) {
if (relationship) {
if (relationship.type === RelationshipType.blocked) throw new HTTPError("You already blocked the user");
relationship.type = RelationshipType.blocked;
} else {
relationship = { id, type: RelationshipType.blocked };
- user.user_data.relationships.push(relationship);
+ newUserRelationships.push(relationship);
}
if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
- friend.user_data.relationships.remove(friendRequest);
+ newFriendRelationships.remove(friendRequest);
await Promise.all([
- friend.save(),
+ UserModel.updateOne({ id: friend.id }, { "user_data.relationships": newFriendRelationships }).exec(),
emitEvent({
event: "RELATIONSHIP_REMOVE",
data: friendRequest,
@@ -50,7 +57,7 @@ router.put("/:id", check({ $type: Number }), async (req: Request, res: Response)
}
await Promise.all([
- user.save(),
+ UserModel.updateOne({ id: req.user_id }, { "user_data.relationships": newUserRelationships }).exec(),
emitEvent({
event: "RELATIONSHIP_ADD",
data: {
@@ -74,17 +81,17 @@ router.put("/:id", check({ $type: Number }), async (req: Request, res: Response)
incoming_relationship = friendRequest;
incoming_relationship.type = RelationshipType.friends;
outgoing_relationship.type = RelationshipType.friends;
- } else friend.user_data.relationships.push(incoming_relationship);
+ } else newFriendRelationships.push(incoming_relationship);
if (relationship) {
if (relationship.type === RelationshipType.outgoing) throw new HTTPError("You already sent a friend request");
if (relationship.type === RelationshipType.blocked) throw new HTTPError("Unblock the user before sending a friend request");
if (relationship.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user");
- } else user.user_data.relationships.push(outgoing_relationship);
+ } else newUserRelationships.push(outgoing_relationship);
await Promise.all([
- user.save(),
- friend.save(),
+ UserModel.updateOne({ id: req.user_id }, { "user_data.relationships": newUserRelationships }).exec(),
+ UserModel.updateOne({ id: friend.id }, { "user_data.relationships": newFriendRelationships }).exec(),
emitEvent({
event: "RELATIONSHIP_ADD",
data: {
@@ -105,6 +112,19 @@ router.put("/:id", check({ $type: Number }), async (req: Request, res: Response)
]);
return res.sendStatus(204);
+}
+
+router.put("/:id", check({ $type: new Length(Number, 1, 4) }), async (req: Request, res: Response) => {
+ return await addRelationship(req, res, await UserModel.findOne({ id: req.params.id }), req.body.type);
+});
+
+router.post("/", check({ discriminator: String, username: String }), async (req: Request, res: Response) => {
+ return await addRelationship(
+ req,
+ res,
+ await UserModel.findOne(req.body as { discriminator: string; username: string }).exec(),
+ req.body.type
+ );
});
router.delete("/:id", async (req: Request, res: Response) => {
diff --git a/src/schema/Channel.ts b/src/schema/Channel.ts
index 2cb7f7f4..48c3a1d2 100644
--- a/src/schema/Channel.ts
+++ b/src/schema/Channel.ts
@@ -3,7 +3,7 @@ import { Length } from "../util/instanceOf";
export const ChannelModifySchema = {
name: new Length(String, 2, 100),
- type: Number,
+ type: new Length(Number, 0, 13),
$topic: new Length(String, 0, 1024),
$bitrate: Number,
$user_limit: Number,
diff --git a/src/schema/Guild.ts b/src/schema/Guild.ts
index 4677efd5..0443e64c 100644
--- a/src/schema/Guild.ts
+++ b/src/schema/Guild.ts
@@ -8,6 +8,7 @@ export const GuildCreateSchema = {
$channels: [Object],
$guild_template_code: String,
$system_channel_id: String,
+ $rules_channel_id: String
};
export interface GuildCreateSchema {
@@ -17,10 +18,13 @@ export interface GuildCreateSchema {
channels?: GuildChannel[];
guild_template_code?: string;
system_channel_id?: string;
+ rules_channel_id?: string;
}
export const GuildUpdateSchema = {
...GuildCreateSchema,
+ name: undefined,
+ $name: new Length(String, 2, 100),
$banner: String,
$splash: String,
$description: String,
@@ -34,6 +38,7 @@ export const GuildUpdateSchema = {
$public_updates_channel_id: String,
$afk_timeout: Number,
$afk_channel_id: String,
+ $preferred_locale: String
};
// @ts-ignore
delete GuildUpdateSchema.$channels;
@@ -50,6 +55,7 @@ export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
public_updates_channel_id?: string;
afk_timeout?: number;
afk_channel_id?: string;
+ preferred_locale?: string;
}
export const GuildGetSchema = {
@@ -96,31 +102,30 @@ export const GuildGetSchema = {
public_updates_channel_id: true,
max_video_channel_users: true,
approximate_member_count: true,
- approximate_presence_count: true,
+ approximate_presence_count: true
// welcome_screen: true,
};
export const GuildTemplateCreateSchema = {
name: String,
- $avatar: String,
-
+ $avatar: String
};
export interface GuildTemplateCreateSchema {
- name: string,
- avatar?: string,
+ name: string;
+ avatar?: string;
}
export const GuildAddChannelToWelcomeScreenSchema = {
channel_id: String,
description: String,
$emoji_id: String,
- emoji_name: String,
-}
+ emoji_name: String
+};
export interface GuildAddChannelToWelcomeScreenSchema {
channel_id: string;
description: string;
emoji_id?: string;
emoji_name: string;
-}
\ No newline at end of file
+}
diff --git a/src/schema/User.ts b/src/schema/User.ts
index ae213ee3..77ee08b4 100644
--- a/src/schema/User.ts
+++ b/src/schema/User.ts
@@ -4,7 +4,8 @@ export const UserModifySchema = {
$username: new Length(String, 2, 32),
$avatar: String,
$bio: new Length(String, 0, 190),
- $accent_color: Number
+ $accent_color: Number,
+ $banner: String
};
export interface UserModifySchema {
@@ -12,4 +13,5 @@ export interface UserModifySchema {
avatar?: string | null;
bio?: string;
accent_color?: number | null;
+ banner?: string | null;
}
diff --git a/src/util/Message.ts b/src/util/Message.ts
index 3e177517..e811f522 100644
--- a/src/util/Message.ts
+++ b/src/util/Message.ts
@@ -25,10 +25,16 @@ const DEFAULT_FETCH_OPTIONS: any = {
};
export async function handleMessage(opts: Partial<Message>) {
- const channel = await ChannelModel.findOne({ id: opts.channel_id }, { guild_id: true, type: true, permission_overwrites: true }).exec();
+ const channel = await ChannelModel.findOne(
+ { id: opts.channel_id },
+ { guild_id: true, type: true, permission_overwrites: true, recipient_ids: true, owner_id: true }
+ )
+ .lean() // lean is needed, because we don't want to populate .recipients that also auto deletes .recipient_ids
+ .exec();
if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404);
// TODO: are tts messages allowed in dm channels? should permission be checked?
+ // @ts-ignore
const permissions = await getPermission(opts.author_id, channel.guild_id, opts.channel_id, { channel });
permissions.hasThrow("SEND_MESSAGES");
if (opts.tts) permissions.hasThrow("SEND_TTS_MESSAGES");
diff --git a/src/util/cdn.ts b/src/util/cdn.ts
index a66e2215..aed8ca0a 100644
--- a/src/util/cdn.ts
+++ b/src/util/cdn.ts
@@ -1,5 +1,6 @@
import { Config } from "@fosscord/server-util";
import FormData from "form-data";
+import { HTTPError } from "lambert-server";
import fetch from "node-fetch";
export async function uploadFile(path: string, file: Express.Multer.File) {
@@ -22,3 +23,18 @@ export async function uploadFile(path: string, file: Express.Multer.File) {
if (response.status !== 200) throw result;
return result;
}
+
+export async function handleFile(path: string, body?: string): Promise<string | undefined> {
+ if (!body || !body.startsWith("data:")) return body;
+ try {
+ const mimetype = body.split(":")[1].split(";")[0];
+ const buffer = Buffer.from(body.split(",")[1], "base64");
+
+ // @ts-ignore
+ const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" });
+ return id;
+ } catch (error) {
+ console.error(error);
+ throw new HTTPError("Invalid " + path);
+ }
+}
diff --git a/src/util/instanceOf.ts b/src/util/instanceOf.ts
index 93a92805..4d9034e5 100644
--- a/src/util/instanceOf.ts
+++ b/src/util/instanceOf.ts
@@ -84,6 +84,8 @@ export function instanceOf(
switch (type) {
case String:
+ value = `${value}`;
+ ref.obj[ref.key] = value;
if (typeof value === "string") return true;
throw new FieldError("BASE_TYPE_STRING", req.t("common:field.BASE_TYPE_STRING"));
case Number:
|