summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Server.ts89
-rw-r--r--src/middlewares/Authentication.ts1
-rw-r--r--src/middlewares/RateLimit.ts6
-rw-r--r--src/middlewares/TestClient.ts66
-rw-r--r--src/routes/auth/login.ts20
-rw-r--r--src/routes/auth/register.ts5
-rw-r--r--src/routes/channels/#channel_id/messages/index.ts20
-rw-r--r--src/routes/guilds/#guild_id/bans.ts2
-rw-r--r--src/routes/guilds/#guild_id/channels.ts7
-rw-r--r--src/routes/guilds/#guild_id/delete.ts18
-rw-r--r--src/routes/guilds/#guild_id/index.ts7
-rw-r--r--src/routes/guilds/#guild_id/vanity-url.ts33
-rw-r--r--src/routes/ping.ts9
-rw-r--r--src/routes/users/#id/profile.ts1
-rw-r--r--src/routes/users/@me/disable.ts16
-rw-r--r--src/routes/users/@me/index.ts17
-rw-r--r--src/routes/users/@me/profile.ts1
-rw-r--r--src/routes/users/@me/relationships.ts60
-rw-r--r--src/schema/Channel.ts2
-rw-r--r--src/schema/Guild.ts21
-rw-r--r--src/schema/User.ts4
-rw-r--r--src/util/Message.ts8
-rw-r--r--src/util/cdn.ts16
-rw-r--r--src/util/instanceOf.ts2
24 files changed, 282 insertions, 149 deletions
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: