summary refs log tree commit diff
path: root/api/src
diff options
context:
space:
mode:
authorFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-01 10:47:00 +0200
committerGitHub <noreply@github.com>2021-09-01 10:47:00 +0200
commitee35e42153d1c57f5964225cccda1abacdc43f48 (patch)
tree5b5b159ae4ec956415a88679ed774b524523e0d9 /api/src
parentMerge pull request #287 from EMREOYUN/patch-1 (diff)
parentMerge pull request #299 from AlTech98/typeorm (diff)
downloadserver-ee35e42153d1c57f5964225cccda1abacdc43f48.tar.xz
Merge pull request #300 from fosscord/typeorm
Diffstat (limited to 'api/src')
-rw-r--r--api/src/Server.ts23
-rw-r--r--api/src/index.ts2
-rw-r--r--api/src/middlewares/Authentication.ts10
-rw-r--r--api/src/middlewares/ErrorHandler.ts21
-rw-r--r--api/src/middlewares/RateLimit.ts145
-rw-r--r--api/src/middlewares/Translation.ts2
-rw-r--r--api/src/routes/auth/login.ts32
-rw-r--r--api/src/routes/auth/register.ts104
-rw-r--r--api/src/routes/channels/#channel_id/index.ts34
-rw-r--r--api/src/routes/channels/#channel_id/invites.ts12
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/ack.ts7
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/index.ts33
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts71
-rw-r--r--api/src/routes/channels/#channel_id/messages/bulk-delete.ts9
-rw-r--r--api/src/routes/channels/#channel_id/messages/index.ts48
-rw-r--r--api/src/routes/channels/#channel_id/permissions.ts42
-rw-r--r--api/src/routes/channels/#channel_id/pins.ts93
-rw-r--r--api/src/routes/channels/#channel_id/typing.ts8
-rw-r--r--api/src/routes/channels/#channel_id/webhooks.ts4
-rw-r--r--api/src/routes/guilds/#guild_id/bans.ts81
-rw-r--r--api/src/routes/guilds/#guild_id/channels.ts24
-rw-r--r--api/src/routes/guilds/#guild_id/delete.ts30
-rw-r--r--api/src/routes/guilds/#guild_id/index.ts40
-rw-r--r--api/src/routes/guilds/#guild_id/invites.ts6
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/index.ts41
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/nick.ts5
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts7
-rw-r--r--api/src/routes/guilds/#guild_id/members/index.ts26
-rw-r--r--api/src/routes/guilds/#guild_id/regions.ts9
-rw-r--r--api/src/routes/guilds/#guild_id/roles.ts86
-rw-r--r--api/src/routes/guilds/#guild_id/templates.ts74
-rw-r--r--api/src/routes/guilds/#guild_id/vanity-url.ts44
-rw-r--r--api/src/routes/guilds/#guild_id/welcome_screen.ts34
-rw-r--r--api/src/routes/guilds/#guild_id/widget.json.ts99
-rw-r--r--api/src/routes/guilds/#guild_id/widget.png.ts4
-rw-r--r--api/src/routes/guilds/#guild_id/widget.ts6
-rw-r--r--api/src/routes/guilds/index.ts104
-rw-r--r--api/src/routes/guilds/templates/index.ts34
-rw-r--r--api/src/routes/invites/index.ts31
-rw-r--r--api/src/routes/users/#id/index.ts5
-rw-r--r--api/src/routes/users/#id/profile.ts36
-rw-r--r--api/src/routes/users/@me/channels.ts34
-rw-r--r--api/src/routes/users/@me/delete.ts17
-rw-r--r--api/src/routes/users/@me/disable.ts13
-rw-r--r--api/src/routes/users/@me/guilds.ts22
-rw-r--r--api/src/routes/users/@me/index.ts49
-rw-r--r--api/src/routes/users/@me/profile.ts36
-rw-r--r--api/src/routes/users/@me/relationships.ts89
-rw-r--r--api/src/routes/users/@me/settings.ts5
-rw-r--r--api/src/routes/voice/regions.ts11
-rw-r--r--api/src/schema/Guild.ts82
-rw-r--r--api/src/schema/Message.ts14
-rw-r--r--api/src/schema/Roles.ts2
-rw-r--r--api/src/test/mongo_test.ts40
-rw-r--r--api/src/test/server_benchmark.ts39
-rw-r--r--api/src/test/test.ts3
-rw-r--r--api/src/util/ApiError.ts23
-rw-r--r--api/src/util/Channel.ts63
-rw-r--r--api/src/util/Constants.ts293
-rw-r--r--api/src/util/Member.ts222
-rw-r--r--api/src/util/Message.ts117
-rw-r--r--api/src/util/User.ts16
-rw-r--r--api/src/util/Voice.ts32
-rw-r--r--api/src/util/ipAddress.ts17
64 files changed, 1210 insertions, 1555 deletions
diff --git a/api/src/Server.ts b/api/src/Server.ts

index adaf7f2c..0f444f86 100644 --- a/api/src/Server.ts +++ b/api/src/Server.ts
@@ -2,7 +2,7 @@ import "missing-native-js-functions"; import { Connection } from "mongoose"; import { Server, ServerOptions } from "lambert-server"; import { Authentication, CORS } from "./middlewares/"; -import { Config, db, initEvent } from "@fosscord/util"; +import { Config, initDatabase, initEvent } from "@fosscord/util"; import { ErrorHandler } from "./middlewares/ErrorHandler"; import { BodyParser } from "./middlewares/BodyParser"; import { Router, Request, Response, NextFunction } from "express"; @@ -31,30 +31,13 @@ export class FosscordServer extends Server { super({ ...opts, errorHandler: false, jsonBody: false }); } - async setupSchema() { - return Promise.all([ - db.collection("users").createIndex({ id: 1 }, { unique: true }), - db.collection("messages").createIndex({ id: 1 }, { unique: true }), - db.collection("channels").createIndex({ id: 1 }, { unique: true }), - db.collection("guilds").createIndex({ id: 1 }, { unique: true }), - db.collection("members").createIndex({ id: 1, guild_id: 1 }, { unique: true }), - db.collection("roles").createIndex({ id: 1 }, { unique: true }), - db.collection("emojis").createIndex({ id: 1 }, { unique: true }), - db.collection("invites").createIndex({ code: 1 }, { unique: true }), - db.collection("invites").createIndex({ expires_at: 1 }, { expireAfterSeconds: 0 }), // after 0 seconds of expires_at the invite will get delete - db.collection("ratelimits").createIndex({ expires_at: 1 }, { expireAfterSeconds: 0 }) - ]); - } - async start() { - // @ts-ignore - await (db as Promise<Connection>); - await this.setupSchema(); + await initDatabase(); await Config.init(); await initEvent(); this.app.use(CORS); - this.app.use(BodyParser({ inflate: true, limit: 1024 * 1024 * 10 })); // 2MB + this.app.use(BodyParser({ inflate: true, limit: 1024 * 1024 * 10 })); // 10MB const app = this.app; const api = Router(); // @ts-ignore diff --git a/api/src/index.ts b/api/src/index.ts
index 73b9dbd2..0bba7f5e 100644 --- a/api/src/index.ts +++ b/api/src/index.ts
@@ -8,8 +8,6 @@ export * from "./schema/Message"; export * from "./util/Constants"; export * from "./util/instanceOf"; export * from "./util/instanceOf"; -export * from "./util/Member"; export * from "./util/RandomInviteID"; export * from "./util/String"; -export * from "./util/User"; export { check as checkPassword } from "./util/passwordStrength"; diff --git a/api/src/middlewares/Authentication.ts b/api/src/middlewares/Authentication.ts
index a8bfe196..a300c786 100644 --- a/api/src/middlewares/Authentication.ts +++ b/api/src/middlewares/Authentication.ts
@@ -18,9 +18,9 @@ export const API_PREFIX_TRAILING_SLASH = /^\/api(\/v\d+)?\//; declare global { namespace Express { interface Request { - user_id: any; + user_id: string; user_bot: boolean; - token: any; + token: string; } } } @@ -28,7 +28,7 @@ declare global { export async function Authentication(req: Request, res: Response, next: NextFunction) { if (req.method === "OPTIONS") return res.sendStatus(204); const url = req.url.replace(API_PREFIX, ""); - if (url.startsWith("/invites") && req.method === "GET") return next(); // @ts-ignore + if (url.startsWith("/invites") && req.method === "GET") return next(); if ( NO_AUTHORIZATION_ROUTES.some((x) => { if (typeof x === "string") return url.startsWith(x); @@ -47,7 +47,7 @@ export async function Authentication(req: Request, res: Response, next: NextFunc req.user_id = decoded.id; req.user_bot = user.bot; return next(); - } catch (error) { - return next(new HTTPError(error.toString(), 400)); + } catch (error: any) { + return next(new HTTPError(error?.toString(), 400)); } } diff --git a/api/src/middlewares/ErrorHandler.ts b/api/src/middlewares/ErrorHandler.ts
index d080e498..f061172a 100644 --- a/api/src/middlewares/ErrorHandler.ts +++ b/api/src/middlewares/ErrorHandler.ts
@@ -1,9 +1,12 @@ import { NextFunction, Request, Response } from "express"; import { HTTPError } from "lambert-server"; +import { EntityNotFoundError } from "typeorm"; import { FieldError } from "../util/instanceOf"; +import {ApiError} from "../util/ApiError"; +// TODO: update with new body/typorm validation export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) { - if (!error) next(); + if (!error) return next(); try { let code = 400; @@ -12,13 +15,23 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne let errors = undefined; if (error instanceof HTTPError && error.code) code = httpcode = error.code; - else if (error instanceof FieldError) { + else if (error instanceof ApiError) { + code = error.code; + message = error.message; + httpcode = error.httpStatus; + } + else if (error instanceof EntityNotFoundError) { + message = `${(error as any).stringifyTarget} can not be found`; + code = 404; + } else if (error instanceof FieldError) { code = Number(error.code); message = error.message; errors = error.errors; } else { - console.error(error); + console.error(`[Error] ${code} ${req.url}`, errors || error, "body:", req.body); + if (req.server?.options?.production) { + // don't expose internal errors to the user, instead human errors should be thrown as HTTPError message = "Internal Server Error"; } code = httpcode = 500; @@ -26,8 +39,6 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne if (httpcode > 511) httpcode = 400; - console.error(`[Error] ${code} ${req.url} ${message}`, errors || error); - res.status(httpcode).json({ code: code, message, errors }); } catch (error) { console.error(`[Internal Server Error] 500`, error); diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts
index acf92606..dffbc0d9 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts
@@ -1,11 +1,12 @@ -// @ts-nocheck -import { db, Bucket, Config, listenEvent, emitEvent } from "@fosscord/util"; +import { Config, listenEvent } from "@fosscord/util"; import { NextFunction, Request, Response, Router } from "express"; import { getIpAdress } from "../util/ipAddress"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; // Docs: https://discord.com/developers/docs/topics/rate-limits +// TODO: use better caching (e.g. redis) as else it creates to much pressure on the database + /* ? bucket limit? Max actions/sec per bucket? @@ -18,10 +19,18 @@ TODO: different for methods (GET/POST) */ -var Cache = new Map<string, Bucket>(); -const EventRateLimit = "ratelimit"; +type RateLimit = { + id: "global" | "error" | string; + executor_id: string; + hits: number; + blocked: boolean; + expires_at: Date; +}; + +var Cache = new Map<string, RateLimit>(); +const EventRateLimit = "RATELIMIT"; -export default function RateLimit(opts: { +export default function rateLimit(opts: { bucket?: string; window: number; count: number; @@ -36,23 +45,32 @@ export default function RateLimit(opts: { }): any { 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.onlyIp && req.user_id) user_id = req.user_id; + var executor_id = getIpAdress(req); + if (!opts.onlyIp && req.user_id) executor_id = req.user_id; var max_hits = opts.count; if (opts.bot && req.user_bot) max_hits = opts.bot; if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET; else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY; - const offender = Cache.get(user_id + bucket_id) as Bucket | null; + const offender = Cache.get(executor_id + bucket_id); - if (offender && offender.blocked) { + if (offender) { const reset = offender.expires_at.getTime(); const resetAfterMs = reset - Date.now(); const resetAfterSec = resetAfterMs / 1000; - const global = bucket_id === "global"; - if (resetAfterMs > 0) { + if (resetAfterMs <= 0) { + offender.hits = 0; + offender.expires_at = new Date(Date.now() + opts.window * 1000); + offender.blocked = false; + + Cache.delete(executor_id + bucket_id); + } + + if (offender.blocked) { + const global = bucket_id === "global"; + console.log("blocked bucket: " + bucket_id, { resetAfterMs }); return ( res @@ -67,17 +85,11 @@ export default function RateLimit(opts: { // TODO: error rate limit message translation .send({ message: "You are being rate limited.", retry_after: resetAfterSec, global }) ); - } else { - offender.hits = 0; - offender.expires_at = new Date(Date.now() + opts.window * 1000); - offender.blocked = false; - // mongodb ttl didn't update yet -> manually update/delete - db.collection("ratelimits").updateOne({ id: bucket_id, user_id }, { $set: offender }); - Cache.delete(user_id + bucket_id); } } + next(); - const hitRouteOpts = { bucket_id, user_id, max_hits, window: opts.window }; + const hitRouteOpts = { bucket_id, executor_id, max_hits, window: opts.window }; if (opts.error || opts.success) { res.once("finish", () => { @@ -97,69 +109,94 @@ export default function RateLimit(opts: { export async function initRateLimits(app: Router) { const { routes, global, ip, error } = Config.get().limits.rate; await listenEvent(EventRateLimit, (event) => { - Cache.set(event.channel_id, event.data); + Cache.set(event.channel_id as string, event.data); event.acknowledge?.(); }); + // await RateLimit.delete({ expires_at: LessThan(new Date().toISOString()) }); // cleans up if not already deleted, morethan -> older date + // const limits = await RateLimit.find({ blocked: true }); + // limits.forEach((limit) => { + // Cache.set(limit.executor_id, limit); + // }); setInterval(() => { Cache.forEach((x, key) => { - if (Date.now() > x.expires_at) Cache.delete(key); + if (new Date() > x.expires_at) { + Cache.delete(key); + // RateLimit.delete({ executor_id: key }); + } }); - }, 1000 * 60 * 10); + }, 1000 * 60); app.use( - RateLimit({ + rateLimit({ bucket: "global", onlyIp: true, ...ip }) ); - app.use(RateLimit({ bucket: "global", ...global })); + app.use(rateLimit({ bucket: "global", ...global })); app.use( - RateLimit({ + rateLimit({ bucket: "error", error: true, onlyIp: true, ...error }) ); - app.use("/guilds/:id", RateLimit(routes.guild)); - app.use("/webhooks/:id", RateLimit(routes.webhook)); - app.use("/channels/:id", RateLimit(routes.channel)); - app.use("/auth/login", RateLimit(routes.auth.login)); - app.use("/auth/register", RateLimit({ onlyIp: true, success: true, ...routes.auth.register })); + app.use("/guilds/:id", rateLimit(routes.guild)); + app.use("/webhooks/:id", rateLimit(routes.webhook)); + app.use("/channels/:id", rateLimit(routes.channel)); + app.use("/auth/login", rateLimit(routes.auth.login)); + app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register })); } -async function hitRoute(opts: { user_id: string; bucket_id: string; max_hits: number; window: number }) { - const filter = { id: opts.bucket_id, user_id: opts.user_id }; - const { value } = await db.collection("ratelimits").findOneAndUpdate( - filter, - { - $setOnInsert: { - id: opts.bucket_id, - user_id: opts.user_id, - expires_at: new Date(Date.now() + opts.window * 1000) - }, - $inc: { - hits: 1 - } - // Conditionally update blocked doesn't work - }, - { upsert: true, returnDocument: "before" } - ); - if (!value) return; - const updateBlock = !value.blocked && value.hits >= opts.max_hits; +async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) { + const id = opts.executor_id + opts.bucket_id; + var limit = Cache.get(id); + if (!limit) { + limit = { + id: opts.bucket_id, + executor_id: opts.executor_id, + expires_at: new Date(Date.now() + opts.window * 1000), + hits: 0, + blocked: false + }; + Cache.set(id, limit); + } + + limit.hits++; + if (limit.hits >= opts.max_hits) { + limit.blocked = true; + } + + /* + var ratelimit = await RateLimit.findOne({ id: opts.bucket_id, executor_id: opts.executor_id }); + if (!ratelimit) { + ratelimit = new RateLimit({ + id: opts.bucket_id, + executor_id: opts.executor_id, + expires_at: new Date(Date.now() + opts.window * 1000), + hits: 0, + blocked: false + }); + } + + ratelimit.hits++; + + const updateBlock = !ratelimit.blocked && ratelimit.hits >= opts.max_hits; if (updateBlock) { - value.blocked = true; - Cache.set(opts.user_id + opts.bucket_id, value); + ratelimit.blocked = true; + Cache.set(opts.executor_id + opts.bucket_id, ratelimit); await emitEvent({ channel_id: EventRateLimit, event: EventRateLimit, - data: value + data: ratelimit }); - await db.collection("ratelimits").updateOne(filter, { $set: { blocked: true } }); } else { - Cache.delete(opts.user_id); + Cache.delete(opts.executor_id); } + + await ratelimit.save(); + */ } diff --git a/api/src/middlewares/Translation.ts b/api/src/middlewares/Translation.ts
index edc14707..baabf221 100644 --- a/api/src/middlewares/Translation.ts +++ b/api/src/middlewares/Translation.ts
@@ -19,7 +19,7 @@ export async function initTranslation(router: Router) { fallbackLng: "en", ns, backend: { - loadPath: __dirname + "/../locales/{{lng}}/{{ns}}.json" + loadPath: __dirname + "/../../locales/{{lng}}/{{ns}}.json" }, load: "all" }); diff --git a/api/src/routes/auth/login.ts b/api/src/routes/auth/login.ts
index dc970e4c..7fd0f870 100644 --- a/api/src/routes/auth/login.ts +++ b/api/src/routes/auth/login.ts
@@ -2,7 +2,7 @@ import { Request, Response, Router } from "express"; import { check, FieldErrors, Length } from "../../util/instanceOf"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; -import { Config, UserModel } from "@fosscord/util"; +import { Config, User } from "@fosscord/util"; import { adjustEmail } from "./register"; const router: Router = Router(); @@ -21,10 +21,7 @@ router.post( async (req: Request, res: Response) => { const { login, password, captcha_key, undelete } = req.body; const email = adjustEmail(login); - const query: any[] = [{ phone: login }]; - if (email) query.push({ email }); - - console.log(req.body, email); + console.log("login", email); const config = Config.get(); @@ -41,27 +38,24 @@ router.post( // TODO: check captcha } - const user = await UserModel.findOne( - { $or: query }, - { "user_data.hash": true, id: true, disabled: true, deleted: true, "user_settings.locale": true, "user_settings.theme": true } - ) - .exec() - .catch((e) => { - console.log(e, query); - throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } }); - }); + const user = await User.findOneOrFail({ + where: [{ phone: login }, { email: login }], + select: ["data", "id", "disabled", "deleted", "settings"] + }).catch((e) => { + throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } }); + }); if (undelete) { // undelete refers to un'disable' here - if (user.disabled) await UserModel.updateOne({ id: user.id }, { disabled: false }).exec(); - if (user.deleted) await UserModel.updateOne({ id: user.id }, { deleted: false }).exec(); + if (user.disabled) await User.update({ id: user.id }, { disabled: false }); + if (user.deleted) await User.update({ id: user.id }, { deleted: false }); } else { if (user.deleted) return res.status(400).json({ message: "This account is scheduled for deletion.", code: 20011 }); 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 || ""); + const same_password = await bcrypt.compare(password, user.data.hash || ""); if (!same_password) { throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); } @@ -72,7 +66,7 @@ router.post( // Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package // https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png - res.json({ token, user_settings: user.user_settings }); + res.json({ token, settings: user.settings }); } ); @@ -106,6 +100,6 @@ export async function generateToken(id: string) { * @returns {"captcha_key": ["captcha-required"], "captcha_sitekey": null, "captcha_service": "recaptcha"} * Sucess: - * @returns {"token": "USERTOKEN", "user_settings": {"locale": "en", "theme": "dark"}} + * @returns {"token": "USERTOKEN", "settings": {"locale": "en", "theme": "dark"}} */ diff --git a/api/src/routes/auth/register.ts b/api/src/routes/auth/register.ts
index fecde874..8bcecda1 100644 --- a/api/src/routes/auth/register.ts +++ b/api/src/routes/auth/register.ts
@@ -1,12 +1,12 @@ import { Request, Response, Router } from "express"; -import { trimSpecial, User, Snowflake, UserModel, Config } from "@fosscord/util"; +import { trimSpecial, User, Snowflake, Config, defaultSettings } from "@fosscord/util"; import bcrypt from "bcrypt"; import { check, Email, EMAIL_REGEX, FieldErrors, Length } from "../../util/instanceOf"; import "missing-native-js-functions"; import { generateToken } from "./login"; import { getIpAdress, IPAnalysis, isProxy } from "../../util/ipAddress"; import { HTTPError } from "lambert-server"; -import RateLimit from "../../middlewares/RateLimit"; +import { In } from "typeorm"; const router: Router = Router(); @@ -55,13 +55,13 @@ router.post( // TODO: check password strength // adjusted_email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick - let adjusted_email: string | null = adjustEmail(email); + let adjusted_email = adjustEmail(email); // adjusted_password will be the hash of the password - let adjusted_password: string = ""; + let adjusted_password = ""; // trim special uf8 control characters -> Backspace, Newline, ... - let adjusted_username: string = trimSpecial(username); + let adjusted_username = trimSpecial(username); // discriminator will be randomly generated let discriminator = ""; @@ -92,9 +92,7 @@ router.post( if (!adjusted_email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } }); // check if there is already an account with this email - const exists = await UserModel.findOne({ email: adjusted_email }) - .exec() - .catch((e) => {}); + const exists = await User.findOneOrFail({ email: adjusted_email }).catch((e) => {}); if (exists) { throw FieldErrors({ @@ -131,9 +129,7 @@ router.post( if (!register.allowMultipleAccounts) { // TODO: check if fingerprint was eligible generated - const exists = await UserModel.findOne({ fingerprints: fingerprint }) - .exec() - .catch((e) => {}); + const exists = await User.findOne({ where: { fingerprints: In(fingerprint) } }); if (exists) { throw FieldErrors({ @@ -168,12 +164,8 @@ router.post( // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database? for (let tries = 0; tries < 5; tries++) { discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); - try { - exists = await UserModel.findOne({ discriminator, username: adjusted_username }, "id").exec(); - } catch (error) { - // doesn't exist -> break - break; - } + exists = await User.findOne({ where: { discriminator, username: adjusted_username }, select: ["id"] }); + if (!exists) break; } if (exists) { @@ -189,96 +181,40 @@ router.post( // appearently discord doesn't save the date of birth and just calculate if nsfw is allowed // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false - const user: User = { - id: Snowflake.generate(), + const user = { created_at: new Date(), username: adjusted_username, discriminator, - avatar: null, - accent_color: null, - banner: null, + id: Snowflake.generate(), bot: false, system: false, desktop: false, mobile: false, premium: true, premium_type: 2, - phone: null, bio: "", mfa_enabled: false, verified: false, disabled: false, deleted: false, - presence: { - activities: [], - client_status: { - desktop: undefined, - mobile: undefined, - web: undefined - }, - status: "offline" - }, email: adjusted_email, nsfw_allowed: true, // TODO: depending on age - public_flags: 0n, - flags: 0n, // TODO: generate default flags - guilds: [], - user_data: { + public_flags: "0", + flags: "0", // TODO: generate + data: { hash: adjusted_password, - valid_tokens_since: new Date(), - relationships: [], - connected_accounts: [], - fingerprints: [] + valid_tokens_since: new Date() }, - user_settings: { - afk_timeout: 300, - allow_accessibility_detection: true, - animate_emoji: true, - animate_stickers: 0, - contact_sync_enabled: false, - convert_emoticons: false, - custom_status: { - emoji_id: null, - emoji_name: null, - expires_at: null, - text: null - }, - default_guilds_restricted: false, - detect_platform_accounts: true, - developer_mode: false, - disable_games_tab: false, - enable_tts_command: true, - explicit_content_filter: 0, - friend_source_flags: { all: true }, - gateway_connected: false, - gif_auto_play: true, - guild_folders: [], - guild_positions: [], - inline_attachment_media: true, - inline_embed_media: true, - locale: req.language, - message_display_compact: false, - native_phone_integration_enabled: true, - render_embeds: true, - render_reactions: true, - restricted_guilds: [], - show_current_game: true, - status: "offline", - stream_notifications_enabled: true, - theme: "dark", - timezone_offset: 0 - // timezone_offset: // TODO: timezone from request - } + settings: defaultSettings, + fingerprints: [] }; - - // insert user into database - await new UserModel(user).save(); + await User.insert(user); return res.json({ token: await generateToken(user.id) }); } ); -export function adjustEmail(email: string): string | null { +export function adjustEmail(email: string): string | undefined { // body parser already checked if it is a valid email const parts = <RegExpMatchArray>email.match(EMAIL_REGEX); // @ts-ignore @@ -304,6 +240,6 @@ export default router; * Field Error * @returns { "code": 50035, "errors": { "consent": { "_errors": [{ "code": "CONSENT_REQUIRED", "message": "You must agree to Discord's Terms of Service and Privacy Policy." }]}}, "message": "Invalid Form Body"} * - * Success 201: + * Success 200: * @returns {token: "OMITTED"} */ diff --git a/api/src/routes/channels/#channel_id/index.ts b/api/src/routes/channels/#channel_id/index.ts
index fb6bcb1a..4aa5a5b9 100644 --- a/api/src/routes/channels/#channel_id/index.ts +++ b/api/src/routes/channels/#channel_id/index.ts
@@ -1,4 +1,4 @@ -import { ChannelDeleteEvent, ChannelModel, ChannelUpdateEvent, emitEvent, getPermission, GuildUpdateEvent, toObject } from "@fosscord/util"; +import { ChannelDeleteEvent, Channel, ChannelUpdateEvent, emitEvent, getPermission } from "@fosscord/util"; import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; import { ChannelModifySchema } from "../../../schema/Channel"; @@ -10,28 +10,28 @@ const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { const { channel_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); const permission = await getPermission(req.user_id, channel.guild_id, channel_id); permission.hasThrow("VIEW_CHANNEL"); - return res.send(toObject(channel)); + return res.send(channel); }); router.delete("/", async (req: Request, res: Response) => { const { channel_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); - const permission = await getPermission(req.user_id, channel?.guild_id, channel_id, { channel }); + const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); permission.hasThrow("MANAGE_CHANNELS"); // TODO: Dm channel "close" not delete - const data = toObject(channel); + const data = channel; await emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent); - await ChannelModel.deleteOne({ id: channel_id }); + await Channel.delete({ id: channel_id }); res.send(data); }); @@ -43,17 +43,19 @@ router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response const permission = await getPermission(req.user_id, undefined, channel_id); permission.hasThrow("MANAGE_CHANNELS"); - const channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, payload, { new: true }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); + channel.assign(payload); - const data = toObject(channel); + await Promise.all([ + channel.save(), + emitEvent({ + event: "CHANNEL_UPDATE", + data: channel, + channel_id + } as ChannelUpdateEvent) + ]); - await emitEvent({ - event: "CHANNEL_UPDATE", - data, - channel_id - } as ChannelUpdateEvent); - - res.send(data); + res.send(channel); }); export default router; diff --git a/api/src/routes/channels/#channel_id/invites.ts b/api/src/routes/channels/#channel_id/invites.ts
index 438f8c51..35006ac4 100644 --- a/api/src/routes/channels/#channel_id/invites.ts +++ b/api/src/routes/channels/#channel_id/invites.ts
@@ -6,14 +6,14 @@ import { random } from "../../../util/RandomInviteID"; import { InviteCreateSchema } from "../../../schema/Invite"; -import { getPermission, ChannelModel, InviteModel, InviteCreateEvent, toObject, emitEvent } from "@fosscord/util"; +import { getPermission, Channel, Invite, InviteCreateEvent, emitEvent } from "@fosscord/util"; const router: Router = Router(); router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) => { const { user_id } = req; const { channel_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); if (!channel.guild_id) { throw new HTTPError("This channel doesn't exist", 404); @@ -38,7 +38,7 @@ router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) inviter_id: user_id }; - await new InviteModel(invite).save(); + await new Invite(invite).save(); await emitEvent({ event: "INVITE_CREATE", data: invite, guild_id } as InviteCreateEvent); res.status(201).send(invite); @@ -47,7 +47,7 @@ router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) router.get("/", async (req: Request, res: Response) => { const { user_id } = req; const { channel_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); if (!channel.guild_id) { throw new HTTPError("This channel doesn't exist", 404); @@ -56,9 +56,9 @@ router.get("/", async (req: Request, res: Response) => { const permission = await getPermission(user_id, guild_id); permission.hasThrow("MANAGE_CHANNELS"); - const invites = await InviteModel.find({ guild_id }).exec(); + const invites = await Invite.find({ guild_id }); - res.status(200).send(toObject(invites)); + res.status(200).send(invites); }); export default router; diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
index bbc779dd..0fd5f2be 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -1,4 +1,4 @@ -import { emitEvent, getPermission, MessageAckEvent, ReadStateModel } from "@fosscord/util"; +import { emitEvent, getPermission, MessageAckEvent, ReadState } from "@fosscord/util"; import { Request, Response, Router } from "express"; import { check } from "../../../../../util/instanceOf"; @@ -14,10 +14,7 @@ router.post("/", check({ $manual: Boolean, $mention_count: Number }), async (req const permission = await getPermission(req.user_id, undefined, channel_id); permission.hasThrow("VIEW_CHANNEL"); - await ReadStateModel.updateOne( - { user_id: req.user_id, channel_id, message_id }, - { user_id: req.user_id, channel_id, message_id } - ).exec(); + await ReadState.update({ user_id: req.user_id, channel_id }, { user_id: req.user_id, channel_id, last_message_id: message_id }); await emitEvent({ event: "MESSAGE_ACK", diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
index 35952d26..b9d46c4f 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -1,6 +1,5 @@ -import { ChannelModel, emitEvent, getPermission, MessageDeleteEvent, MessageModel, MessageUpdateEvent, toObject } from "@fosscord/util"; +import { Channel, emitEvent, getPermission, MessageDeleteEvent, Message, MessageUpdateEvent } from "@fosscord/util"; import { Router, Response, Request } from "express"; -import { HTTPError } from "lambert-server"; import { MessageCreateSchema } from "../../../../../schema/Message"; import { check } from "../../../../../util/instanceOf"; @@ -12,7 +11,7 @@ router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response const { message_id, channel_id } = req.params; var body = req.body as MessageCreateSchema; - var message = await MessageModel.findOne({ id: message_id, channel_id }, { author_id: true, message_reference: true }).lean().exec(); + const message = await Message.findOneOrFail({ id: message_id, channel_id }); const permissions = await getPermission(req.user_id, undefined, channel_id); @@ -21,7 +20,9 @@ router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response body = { flags: body.flags }; // admins can only suppress embeds of other messages } - const opts = await handleMessage({ + const new_message = await handleMessage({ + // TODO: should be message_reference overridable? + // @ts-ignore message_reference: message.message_reference, ...body, author_id: message.author_id, @@ -30,18 +31,18 @@ router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response edited_timestamp: new Date() }); - // @ts-ignore - message = await MessageModel.findOneAndUpdate({ id: message_id }, opts, { new: true }).populate("author").exec(); - - await emitEvent({ - event: "MESSAGE_UPDATE", - channel_id, - data: { ...toObject(message), nonce: undefined } - } as MessageUpdateEvent); + await Promise.all([ + new_message.save(), + await emitEvent({ + event: "MESSAGE_UPDATE", + channel_id, + data: { ...message, nonce: undefined } + } as MessageUpdateEvent) + ]); postHandleMessage(message); - return res.json(toObject(message)); + return res.json(message); }); // TODO: delete attachments in message @@ -49,13 +50,13 @@ router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response router.delete("/", async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }); - const message = await MessageModel.findOne({ id: message_id }, { author_id: true }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); + const message = await Message.findOneOrFail({ id: message_id }); const permission = await getPermission(req.user_id, channel.guild_id, channel_id); if (message.author_id !== req.user_id) permission.hasThrow("MANAGE_MESSAGES"); - await MessageModel.deleteOne({ id: message_id }).exec(); + await Message.delete({ id: message_id }); await emitEvent({ event: "MESSAGE_DELETE", diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts b/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
index 7da63644..f60484b5 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -1,21 +1,21 @@ import { - ChannelModel, + Channel, emitEvent, - EmojiModel, + Emoji, getPermission, - MemberModel, - MessageModel, + Member, + Message, MessageReactionAddEvent, MessageReactionRemoveAllEvent, MessageReactionRemoveEmojiEvent, MessageReactionRemoveEvent, PartialEmoji, PublicUserProjection, - toObject, - UserModel + User } from "@fosscord/util"; import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; +import { In } from "typeorm"; const router = Router(); // TODO: check if emoji is really an unicode emoji or a prperly encoded external emoji @@ -38,12 +38,12 @@ function getEmoji(emoji: string): PartialEmoji { router.delete("/", async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); const permissions = await getPermission(req.user_id, undefined, channel_id); permissions.hasThrow("MANAGE_MESSAGES"); - await MessageModel.findOneAndUpdate({ id: message_id, channel_id }, { reactions: [] }, { new: true }).exec(); + await Message.update({ id: message_id, channel_id }, { reactions: [] }); await emitEvent({ event: "MESSAGE_REACTION_REMOVE_ALL", @@ -62,29 +62,28 @@ router.delete("/:emoji", async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); - const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec(); - const permissions = await getPermission(req.user_id, undefined, channel_id); permissions.hasThrow("MANAGE_MESSAGES"); - const message = await MessageModel.findOne({ id: message_id, channel_id }).exec(); + const message = await Message.findOneOrFail({ id: message_id, channel_id }); const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); if (!already_added) throw new HTTPError("Reaction not found", 404); message.reactions.remove(already_added); - await MessageModel.updateOne({ id: message_id, channel_id }, message).exec(); - - await emitEvent({ - event: "MESSAGE_REACTION_REMOVE_EMOJI", - channel_id, - data: { + await Promise.all([ + message.save(), + emitEvent({ + event: "MESSAGE_REACTION_REMOVE_EMOJI", channel_id, - message_id, - guild_id: channel.guild_id, - emoji - } - } as MessageReactionRemoveEmojiEvent); + data: { + channel_id, + message_id, + guild_id: message.guild_id, + emoji + } + } as MessageReactionRemoveEmojiEvent) + ]); res.sendStatus(204); }); @@ -93,17 +92,21 @@ router.get("/:emoji", async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); - const message = await MessageModel.findOne({ id: message_id, channel_id }).exec(); - if (!message) throw new HTTPError("Message not found", 404); + const message = await Message.findOneOrFail({ id: message_id, channel_id }); const reaction = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); if (!reaction) throw new HTTPError("Reaction not found", 404); const permissions = await getPermission(req.user_id, undefined, channel_id); permissions.hasThrow("VIEW_CHANNEL"); - const users = await UserModel.find({ id: { $in: reaction.user_ids } }, PublicUserProjection).exec(); + const users = await User.find({ + where: { + id: In(reaction.user_ids) + }, + select: PublicUserProjection + }); - res.json(toObject(users)); + res.json(users); }); router.put("/:emoji/:user_id", async (req: Request, res: Response) => { @@ -111,8 +114,8 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => { if (user_id !== "@me") throw new HTTPError("Invalid user"); const emoji = getEmoji(req.params.emoji); - const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec(); - const message = await MessageModel.findOne({ id: message_id, channel_id }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); + const message = await Message.findOneOrFail({ id: message_id, channel_id }); const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); const permissions = await getPermission(req.user_id, undefined, channel_id); @@ -120,7 +123,7 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => { if (!already_added) permissions.hasThrow("ADD_REACTIONS"); if (emoji.id) { - const external_emoji = await EmojiModel.findOne({ id: emoji.id }).exec(); + const external_emoji = await Emoji.findOneOrFail({ id: emoji.id }); if (!already_added) permissions.hasThrow("USE_EXTERNAL_EMOJIS"); emoji.animated = external_emoji.animated; emoji.name = external_emoji.name; @@ -131,9 +134,9 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => { already_added.count++; } else message.reactions.push({ count: 1, emoji, user_ids: [req.user_id] }); - await MessageModel.updateOne({ id: message_id, channel_id }, message).exec(); + await Message.update({ id: message_id, channel_id }, message); - const member = channel.guild_id && (await MemberModel.findOne({ id: req.user_id }).exec()); + const member = channel.guild_id && (await Member.findOneOrFail({ id: req.user_id })); await emitEvent({ event: "MESSAGE_REACTION_ADD", @@ -156,8 +159,8 @@ router.delete("/:emoji/:user_id", async (req: Request, res: Response) => { const emoji = getEmoji(req.params.emoji); - const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec(); - const message = await MessageModel.findOne({ id: message_id, channel_id }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); + const message = await Message.findOneOrFail({ id: message_id, channel_id }); const permissions = await getPermission(req.user_id, undefined, channel_id); @@ -171,7 +174,7 @@ router.delete("/:emoji/:user_id", async (req: Request, res: Response) => { if (already_added.count <= 0) message.reactions.remove(already_added); - await MessageModel.updateOne({ id: message_id, channel_id }, message).exec(); + await Message.update({ id: message_id, channel_id }, message); await emitEvent({ event: "MESSAGE_REACTION_REMOVE", diff --git a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts
index 8132462f..5c486676 100644 --- a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -1,8 +1,9 @@ import { Router, Response, Request } from "express"; -import { ChannelModel, Config, emitEvent, getPermission, MessageDeleteBulkEvent, MessageModel } from "@fosscord/util"; +import { Channel, Config, emitEvent, getPermission, MessageDeleteBulkEvent, Message } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { check } from "../../../../util/instanceOf"; +import { In } from "typeorm"; const router: Router = Router(); @@ -13,10 +14,10 @@ export default router; // https://discord.com/developers/docs/resources/channel#bulk-delete-messages router.post("/", check({ messages: [String] }), async (req: Request, res: Response) => { const { channel_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }, { permission_overwrites: true, guild_id: true }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400); - const permission = await getPermission(req.user_id, channel?.guild_id, channel_id, { channel }); + const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); permission.hasThrow("MANAGE_MESSAGES"); const { maxBulkDelete } = Config.get().limits.message; @@ -25,7 +26,7 @@ router.post("/", check({ messages: [String] }), async (req: Request, res: Respon if (messages.length < 2) throw new HTTPError("You must at least specify 2 messages to bulk delete"); if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`); - await MessageModel.deleteMany({ id: { $in: messages } }).exec(); + await Message.delete({ id: In(messages) }); await emitEvent({ event: "MESSAGE_DELETE_BULK", diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index 6ae6491f..86de6de8 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -1,5 +1,5 @@ import { Router, Response, Request } from "express"; -import { Attachment, ChannelModel, ChannelType, getPermission, MessageDocument, MessageModel, toObject } from "@fosscord/util"; +import { Attachment, Channel, ChannelType, getPermission, Message } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { MessageCreateSchema } from "../../../../schema/Message"; import { check, instanceOf, Length } from "../../../../util/instanceOf"; @@ -7,6 +7,7 @@ import multer from "multer"; import { Query } from "mongoose"; import { sendMessage } from "../../../../util/Message"; import { uploadFile } from "../../../../util/cdn"; +import { FindManyOptions, LessThan, MoreThan } from "typeorm"; const router: Router = Router(); @@ -30,12 +31,7 @@ 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, 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(); + const channel = await Channel.findOneOrFail({ id: channel_id }); if (!channel) throw new HTTPError("Channel not found", 404); isTextChannel(channel.type); @@ -57,32 +53,34 @@ router.get("/", async (req: Request, res: Response) => { permissions.hasThrow("VIEW_CHANNEL"); if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); - var query: Query<MessageDocument[], MessageDocument>; - if (after) query = MessageModel.find({ channel_id, id: { $gt: after } }); - else if (before) query = MessageModel.find({ channel_id, id: { $lt: before } }); - else if (around) - query = MessageModel.find({ - channel_id, - id: { $gt: (BigInt(around) - BigInt(halfLimit)).toString(), $lt: (BigInt(around) + BigInt(halfLimit)).toString() } - }); - else { - query = MessageModel.find({ channel_id }); + var query: FindManyOptions<Message> & { where: { id?: any } } = { + order: { id: "DESC" }, + take: limit, + where: { channel_id }, + relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] + }; + + if (after) query.where.id = MoreThan(after); + else if (before) query.where.id = LessThan(before); + else if (around) { + query.where.id = [ + MoreThan((BigInt(around) - BigInt(halfLimit)).toString()), + LessThan((BigInt(around) + BigInt(halfLimit)).toString()) + ]; } - query = query.sort({ id: -1 }); - - const messages = await query.limit(limit).exec(); + const messages = await Message.find(query); return res.json( - toObject(messages).map((x) => { - (x.reactions || []).forEach((x) => { + messages.map((x) => { + (x.reactions || []).forEach((x: any) => { // @ts-ignore if ((x.user_ids || []).includes(req.user_id)) x.me = true; // @ts-ignore delete x.user_ids; }); // @ts-ignore - if (!x.author) x.author = { discriminator: "0000", username: "Deleted User", public_flags: 0n, avatar: null }; + if (!x.author) x.author = { discriminator: "0000", username: "Deleted User", public_flags: "0", avatar: null }; return x; }) @@ -139,8 +137,8 @@ router.post("/", messageUpload.single("file"), async (req: Request, res: Respons embeds, channel_id, attachments, - edited_timestamp: null + edited_timestamp: undefined }); - return res.send(data); + return res.json(data); }); diff --git a/api/src/routes/channels/#channel_id/permissions.ts b/api/src/routes/channels/#channel_id/permissions.ts
index f93075b1..9c49542b 100644 --- a/api/src/routes/channels/#channel_id/permissions.ts +++ b/api/src/routes/channels/#channel_id/permissions.ts
@@ -1,13 +1,4 @@ -import { - ChannelModel, - ChannelPermissionOverwrite, - ChannelUpdateEvent, - emitEvent, - getPermission, - MemberModel, - RoleModel, - toObject -} from "@fosscord/util"; +import { Channel, ChannelPermissionOverwrite, ChannelUpdateEvent, emitEvent, getPermission, Member, Role } from "@fosscord/util"; import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; @@ -20,16 +11,16 @@ router.put("/:overwrite_id", check({ allow: String, deny: String, type: Number, const { channel_id, overwrite_id } = req.params; const body = req.body as { allow: bigint; deny: bigint; type: number; id: string }; - var channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, permission_overwrites: true }).exec(); + var channel = await Channel.findOneOrFail({ id: channel_id }); if (!channel.guild_id) throw new HTTPError("Channel not found", 404); const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); permissions.hasThrow("MANAGE_ROLES"); if (body.type === 0) { - if (!(await RoleModel.exists({ id: overwrite_id }))) throw new HTTPError("role not found", 404); + if (!(await Role.count({ id: overwrite_id }))) throw new HTTPError("role not found", 404); } else if (body.type === 1) { - if (!(await MemberModel.exists({ id: overwrite_id }))) throw new HTTPError("user not found", 404); + if (!(await Member.count({ id: overwrite_id }))) throw new HTTPError("user not found", 404); } else throw new HTTPError("type not supported", 501); // @ts-ignore @@ -48,12 +39,12 @@ router.put("/:overwrite_id", check({ allow: String, deny: String, type: Number, overwrite.deny = body.deny; // @ts-ignore - channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, channel, { new: true }).exec(); + channel = await Channel.findOneOrFailAndUpdate({ id: channel_id }, channel, { new: true }); await emitEvent({ event: "CHANNEL_UPDATE", channel_id, - data: toObject(channel) + data: channel } as ChannelUpdateEvent); return res.sendStatus(204); @@ -66,18 +57,19 @@ router.delete("/:overwrite_id", async (req: Request, res: Response) => { const permissions = await getPermission(req.user_id, undefined, channel_id); permissions.hasThrow("MANAGE_ROLES"); - const channel = await ChannelModel.findOneAndUpdate( - { id: channel_id }, - { $pull: { permission_overwrites: { id: overwrite_id } } }, - { new: true } - ); + const channel = await Channel.findOneOrFail({ id: channel_id }); if (!channel.guild_id) throw new HTTPError("Channel not found", 404); - await emitEvent({ - event: "CHANNEL_UPDATE", - channel_id, - data: toObject(channel) - } as ChannelUpdateEvent); + channel.permission_overwrites = channel.permission_overwrites.filter((x) => x.id === overwrite_id); + + await Promise.all([ + channel.save(), + emitEvent({ + event: "CHANNEL_UPDATE", + channel_id, + data: channel + } as ChannelUpdateEvent) + ]); return res.sendStatus(204); }); diff --git a/api/src/routes/channels/#channel_id/pins.ts b/api/src/routes/channels/#channel_id/pins.ts
index 0dd81bd3..96a3fdbf 100644 --- a/api/src/routes/channels/#channel_id/pins.ts +++ b/api/src/routes/channels/#channel_id/pins.ts
@@ -1,13 +1,4 @@ -import { - ChannelModel, - ChannelPinsUpdateEvent, - Config, - emitEvent, - getPermission, - MessageModel, - MessageUpdateEvent, - toObject -} from "@fosscord/util"; +import { Channel, ChannelPinsUpdateEvent, Config, emitEvent, getPermission, Message, MessageUpdateEvent } from "@fosscord/util"; import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; @@ -15,35 +6,36 @@ const router: Router = Router(); router.put("/:message_id", async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }).exec(); - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); + + const message = await Message.findOneOrFail({ id: message_id }); + const permission = await getPermission(req.user_id, message.guild_id, channel_id); permission.hasThrow("VIEW_CHANNEL"); // * in dm channels anyone can pin messages -> only check for guilds - if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES"); + if (message.guild_id) permission.hasThrow("MANAGE_MESSAGES"); - const pinned_count = await MessageModel.count({ channel_id, pinned: true }).exec(); + const pinned_count = await Message.count({ channel: { id: channel_id }, pinned: true }); const { maxPins } = Config.get().limits.channel; if (pinned_count >= maxPins) throw new HTTPError("Max pin count reached: " + maxPins); - await MessageModel.updateOne({ id: message_id }, { pinned: true }).exec(); - const message = toObject(await MessageModel.findOne({ id: message_id }).exec()); - - await emitEvent({ - event: "MESSAGE_UPDATE", - channel_id, - data: message - } as MessageUpdateEvent); + await Promise.all([ + Message.update({ id: message_id }, { pinned: true }), + emitEvent({ + event: "MESSAGE_UPDATE", + channel_id, + data: message + } as MessageUpdateEvent), - await emitEvent({ - event: "CHANNEL_PINS_UPDATE", - channel_id, - data: { + emitEvent({ + event: "CHANNEL_PINS_UPDATE", channel_id, - guild_id: channel.guild_id, - last_pin_timestamp: undefined - } - } as ChannelPinsUpdateEvent); + data: { + channel_id, + guild_id: message.guild_id, + last_pin_timestamp: undefined + } + } as ChannelPinsUpdateEvent) + ]); res.sendStatus(204); }); @@ -51,29 +43,34 @@ router.put("/:message_id", async (req: Request, res: Response) => { router.delete("/:message_id", async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); const permission = await getPermission(req.user_id, channel.guild_id, channel_id); permission.hasThrow("VIEW_CHANNEL"); if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES"); - const message = toObject(await MessageModel.findOneAndUpdate({ id: message_id }, { pinned: false }, { new: true }).exec()); + const message = await Message.findOneOrFail({ id: message_id }); + message.pinned = false; + + await Promise.all([ + message.save(), - await emitEvent({ - event: "MESSAGE_UPDATE", - channel_id, - data: message - } as MessageUpdateEvent); + emitEvent({ + event: "MESSAGE_UPDATE", + channel_id, + data: message + } as MessageUpdateEvent), - await emitEvent({ - event: "CHANNEL_PINS_UPDATE", - channel_id, - data: { + emitEvent({ + event: "CHANNEL_PINS_UPDATE", channel_id, - guild_id: channel.guild_id, - last_pin_timestamp: undefined - } - } as ChannelPinsUpdateEvent); + data: { + channel_id, + guild_id: channel.guild_id, + last_pin_timestamp: undefined + } + } as ChannelPinsUpdateEvent) + ]); res.sendStatus(204); }); @@ -81,13 +78,13 @@ router.delete("/:message_id", async (req: Request, res: Response) => { router.get("/", async (req: Request, res: Response) => { const { channel_id } = req.params; - const channel = await ChannelModel.findOne({ id: channel_id }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); const permission = await getPermission(req.user_id, channel.guild_id, channel_id); permission.hasThrow("VIEW_CHANNEL"); - let pins = await MessageModel.find({ channel_id: channel_id, pinned: true }).exec(); + let pins = await Message.find({ channel_id: channel_id, pinned: true }); - res.send(toObject(pins)); + res.send(pins); }); export default router; diff --git a/api/src/routes/channels/#channel_id/typing.ts b/api/src/routes/channels/#channel_id/typing.ts
index 21d453d8..f1fb3c86 100644 --- a/api/src/routes/channels/#channel_id/typing.ts +++ b/api/src/routes/channels/#channel_id/typing.ts
@@ -1,4 +1,4 @@ -import { ChannelModel, emitEvent, MemberModel, toObject, TypingStartEvent } from "@fosscord/util"; +import { Channel, emitEvent, Member, TypingStartEvent } from "@fosscord/util"; import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; @@ -9,15 +9,15 @@ router.post("/", async (req: Request, res: Response) => { const { channel_id } = req.params; const user_id = req.user_id; const timestamp = Date.now(); - const channel = await ChannelModel.findOne({ id: channel_id }); - const member = await MemberModel.findOne({ id: user_id }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); + const member = await Member.findOneOrFail({ id: user_id }); await emitEvent({ event: "TYPING_START", channel_id: channel_id, data: { // this is the paylod - member: toObject(member), + member: { ...member, roles: member.roles.map((x) => x.id) }, channel_id, timestamp, user_id, diff --git a/api/src/routes/channels/#channel_id/webhooks.ts b/api/src/routes/channels/#channel_id/webhooks.ts
index 7852f8f3..775053ba 100644 --- a/api/src/routes/channels/#channel_id/webhooks.ts +++ b/api/src/routes/channels/#channel_id/webhooks.ts
@@ -1,6 +1,6 @@ import { Router, Response, Request } from "express"; import { check, Length } from "../../../util/instanceOf"; -import { ChannelModel, getPermission, trimSpecial } from "@fosscord/util"; +import { Channel, getPermission, trimSpecial } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { isTextChannel } from "./messages/index"; @@ -10,7 +10,7 @@ const router: Router = Router(); // TODO: use Image Data Type for avatar instead of String router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), 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 }).exec(); + const channel = await Channel.findOneOrFail({ id: channel_id }); isTextChannel(channel.type); if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400); diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts
index bb3eac03..b84a68a7 100644 --- a/api/src/routes/guilds/#guild_id/bans.ts +++ b/api/src/routes/guilds/#guild_id/bans.ts
@@ -1,30 +1,25 @@ import { Request, Response, Router } from "express"; -import { BanModel, emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, GuildModel, toObject } from "@fosscord/util"; +import { emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { getIpAdress } from "../../../util/ipAddress"; import { BanCreateSchema } from "../../../schema/Ban"; import { check } from "../../../util/instanceOf"; -import { removeMember } from "../../../util/Member"; -import { getPublicUser } from "../../../util/User"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { const { guild_id } = req.params; - 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_id: true, reason: true }).exec(); - return res.json(toObject(bans)); + var bans = await Ban.find({ guild_id: guild_id }); + return res.json(bans); }); router.get("/:user", async (req: Request, res: Response) => { const { guild_id } = req.params; const user_id = req.params.ban; - var ban = await BanModel.findOne({ guild_id: guild_id, user_id: user_id }).exec(); + var ban = await Ban.findOneOrFail({ guild_id: guild_id, user_id: user_id }); return res.json(ban); }); @@ -32,57 +27,59 @@ router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Respon const { guild_id } = req.params; const banned_user_id = req.params.user_id; - const banned_user = await getPublicUser(banned_user_id); + const banned_user = await User.getPublicUser(banned_user_id); const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("BAN_MEMBERS"); if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400); + if (perms.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); - await removeMember(banned_user_id, guild_id); - - const ban = await new BanModel({ + const ban = new Ban({ user_id: banned_user_id, guild_id: guild_id, ip: getIpAdress(req), executor_id: req.user_id, reason: req.body.reason // || otherwise empty - }).save(); - - await emitEvent({ - event: "GUILD_BAN_ADD", - data: { - guild_id: guild_id, - user: banned_user - }, - guild_id: guild_id - } as GuildBanAddEvent); - - return res.json(toObject(ban)); + }); + + await Promise.all([ + Member.removeFromGuild(banned_user_id, guild_id), + ban.save(), + emitEvent({ + event: "GUILD_BAN_ADD", + data: { + guild_id: guild_id, + user: banned_user + }, + guild_id: guild_id + } as GuildBanAddEvent) + ]); + + return res.json(ban); }); router.delete("/:user_id", async (req: Request, res: Response) => { var { guild_id } = req.params; var banned_user_id = req.params.user_id; - const banned_user = await getPublicUser(banned_user_id); - const guild = await GuildModel.exists({ id: guild_id }); - if (!guild) throw new HTTPError("Guild not found", 404); - + const banned_user = await User.getPublicUser(banned_user_id); const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("BAN_MEMBERS"); - await BanModel.deleteOne({ - user_id: banned_user_id, - guild_id - }).exec(); - - await emitEvent({ - event: "GUILD_BAN_REMOVE", - data: { - guild_id, - user: banned_user - }, - guild_id - } as GuildBanRemoveEvent); + await Promise.all([ + Ban.delete({ + user_id: banned_user_id, + guild_id + }), + + emitEvent({ + event: "GUILD_BAN_REMOVE", + data: { + guild_id, + user: banned_user + }, + guild_id + } as GuildBanRemoveEvent) + ]); return res.status(204).send(); }); diff --git a/api/src/routes/guilds/#guild_id/channels.ts b/api/src/routes/guilds/#guild_id/channels.ts
index 1c55ef24..5aa1d33d 100644 --- a/api/src/routes/guilds/#guild_id/channels.ts +++ b/api/src/routes/guilds/#guild_id/channels.ts
@@ -1,17 +1,16 @@ import { Router, Response, Request } from "express"; -import { ChannelModel, toObject, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; +import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { ChannelModifySchema } from "../../../schema/Channel"; import { check } from "../../../util/instanceOf"; -import { createChannel } from "../../../util/Channel"; const router = Router(); router.get("/", async (req: Request, res: Response) => { const { guild_id } = req.params; - const channels = await ChannelModel.find({ guild_id }).exec(); + const channels = await Channel.find({ guild_id }); - res.json(toObject(channels)); + res.json(channels); }); // TODO: check if channel type is permitted @@ -22,9 +21,9 @@ 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); + const channel = await Channel.createChannel({ ...body, guild_id }, req.user_id); - res.status(201).json(toObject(channel)); + res.status(201).json(channel); }); // TODO: check if parent_id exists @@ -48,18 +47,19 @@ router.patch( if (x.parent_id) { opts.parent_id = x.parent_id; - const parent_channel = await ChannelModel.findOne( - { id: x.parent_id, guild_id }, - { permission_overwrites: true } - ).exec(); + const parent_channel = await Channel.findOneOrFail({ + where: { id: x.parent_id, guild_id }, + select: ["permission_overwrites"] + }); if (x.lock_permissions) { opts.permission_overwrites = parent_channel.permission_overwrites; } } - const channel = await ChannelModel.findOneAndUpdate({ id: x.id, guild_id }, opts, { new: true }).exec(); + await Channel.update({ guild_id, id: x.id }, opts); + const channel = await Channel.findOneOrFail({ guild_id, id: x.id }); - await emitEvent({ event: "CHANNEL_UPDATE", data: toObject(channel), channel_id: x.id, guild_id } as ChannelUpdateEvent); + await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: x.id, guild_id } as ChannelUpdateEvent); }) ]); diff --git a/api/src/routes/guilds/#guild_id/delete.ts b/api/src/routes/guilds/#guild_id/delete.ts
index ba1c2fde..043260e9 100644 --- a/api/src/routes/guilds/#guild_id/delete.ts +++ b/api/src/routes/guilds/#guild_id/delete.ts
@@ -1,15 +1,4 @@ -import { - ChannelModel, - emitEvent, - EmojiModel, - GuildDeleteEvent, - GuildModel, - InviteModel, - MemberModel, - MessageModel, - RoleModel, - UserModel -} from "@fosscord/util"; +import { Channel, emitEvent, GuildDeleteEvent, Guild, Member, Message, Role, Invite, Emoji } from "@fosscord/util"; import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; @@ -20,7 +9,7 @@ const router = Router(); router.post("/", async (req: Request, res: Response) => { var { guild_id } = req.params; - const guild = await GuildModel.findOne({ id: guild_id }, "owner_id").exec(); + const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] }); if (guild.owner_id !== req.user_id) throw new HTTPError("You are not the owner of this guild", 401); await emitEvent({ @@ -32,14 +21,13 @@ router.post("/", async (req: Request, res: Response) => { } as GuildDeleteEvent); 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() + Guild.delete({ id: guild_id }), + Role.delete({ guild_id }), + Channel.delete({ guild_id }), + Emoji.delete({ guild_id }), + Invite.delete({ guild_id }), + Message.delete({ guild_id }), + Member.delete({ guild_id }) ]); return res.sendStatus(204); diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts
index 87103caa..6f55be3b 100644 --- a/api/src/routes/guilds/#guild_id/index.ts +++ b/api/src/routes/guilds/#guild_id/index.ts
@@ -1,19 +1,5 @@ import { Request, Response, Router } from "express"; -import { - ChannelModel, - emitEvent, - EmojiModel, - getPermission, - GuildDeleteEvent, - GuildModel, - GuildUpdateEvent, - InviteModel, - MemberModel, - MessageModel, - RoleModel, - toObject, - UserModel -} from "@fosscord/util"; +import { emitEvent, getPermission, Guild, GuildUpdateEvent, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { GuildUpdateSchema } from "../../../schema/Guild"; @@ -26,12 +12,15 @@ const router = Router(); router.get("/", async (req: Request, res: Response) => { const { guild_id } = req.params; - const guild = await GuildModel.findOne({ id: guild_id }) - .populate({ path: "joined_at", match: { id: req.user_id } }) - .exec(); + const [guild, member_count, member] = await Promise.all([ + Guild.findOneOrFail({ id: guild_id }), + Member.count({ guild: { id: guild_id }, id: req.user_id }), + Member.findOneOrFail({ id: req.user_id }) + ]); + if (!member_count) throw new HTTPError("You are not a member of the guild you are trying to access", 401); - const member = await MemberModel.exists({ guild_id: guild_id, id: req.user_id }); - if (!member) throw new HTTPError("You are not a member of the guild you are trying to access", 401); + // @ts-ignore + guild.joined_at = member?.joined_at; return res.json(guild); }); @@ -48,15 +37,12 @@ router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response) 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, { new: true }) - .populate({ path: "joined_at", match: { id: req.user_id } }) - .exec(); + const guild = await Guild.findOneOrFail({ id: guild_id }); + guild.assign(body); - const data = toObject(guild); + await Promise.all([guild.save(), emitEvent({ event: "GUILD_UPDATE", data: guild, guild_id } as GuildUpdateEvent)]); - emitEvent({ event: "GUILD_UPDATE", data: data, guild_id } as GuildUpdateEvent); - - return res.json(data); + return res.json(guild); }); export default router; diff --git a/api/src/routes/guilds/#guild_id/invites.ts b/api/src/routes/guilds/#guild_id/invites.ts
index 08048d61..1843b689 100644 --- a/api/src/routes/guilds/#guild_id/invites.ts +++ b/api/src/routes/guilds/#guild_id/invites.ts
@@ -1,4 +1,4 @@ -import { getPermission, InviteModel, toObject } from "@fosscord/util"; +import { getPermission, Invite } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); @@ -9,9 +9,9 @@ router.get("/", async (req: Request, res: Response) => { const permissions = await getPermission(req.user_id, guild_id); permissions.hasThrow("MANAGE_GUILD"); - const invites = await InviteModel.find({ guild_id }).exec(); + const invites = await Invite.find({ guild_id }); - return res.json(toObject(invites)); + return res.json(invites); }); export default router; diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
index 515434d6..d9ce91c0 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -1,50 +1,53 @@ import { Request, Response, Router } from "express"; import { - GuildModel, - MemberModel, - UserModel, - toObject, + Guild, + Member, + User, GuildMemberAddEvent, getPermission, PermissionResolvable, - RoleModel, + Role, GuildMemberUpdateEvent, emitEvent } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { addMember, isMember, removeMember } from "../../../../../util/Member"; import { check } from "../../../../../util/instanceOf"; import { MemberChangeSchema } from "../../../../../schema/Member"; +import { In } from "typeorm"; const router = Router(); router.get("/", async (req: Request, res: Response) => { const { guild_id, member_id } = req.params; - await isMember(req.user_id, guild_id); + await Member.IsInGuildOrFail(req.user_id, guild_id); - const member = await MemberModel.findOne({ id: member_id, guild_id }).exec(); + const member = await Member.findOneOrFail({ id: member_id, guild_id }); - return res.json(toObject(member)); + return res.json(member); }); router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) => { const { guild_id, member_id } = req.params; const body = req.body as MemberChangeSchema; if (body.roles) { - const roles = await RoleModel.find({ id: { $in: body.roles } }).exec(); + const roles = await Role.find({ id: In(body.roles) }); if (body.roles.length !== roles.length) throw new HTTPError("Roles not found", 404); // TODO: check if user has permission to add role } - const member = await MemberModel.findOneAndUpdate({ id: member_id, guild_id }, body, { new: true }).exec(); + const member = await Member.findOneOrFail({ id: member_id, guild_id }); + member.assign(req.body); - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - guild_id, - data: toObject(member) - } as GuildMemberUpdateEvent); + Promise.all([ + member.save(), + emitEvent({ + event: "GUILD_MEMBER_UPDATE", + guild_id, + data: { ...member, roles: member.roles.map((x) => x.id) } + } as GuildMemberUpdateEvent) + ]); - res.json(toObject(member)); + res.json(member); }); router.put("/", async (req: Request, res: Response) => { @@ -52,7 +55,7 @@ router.put("/", async (req: Request, res: Response) => { throw new HTTPError("Maintenance: Currently you can't add a member", 403); // TODO: only for oauth2 applications - await addMember(member_id, guild_id); + await Member.addToGuild(member_id, guild_id); res.sendStatus(204); }); @@ -62,7 +65,7 @@ router.delete("/", async (req: Request, res: Response) => { const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("KICK_MEMBERS"); - await removeMember(member_id, guild_id); + await Member.removeFromGuild(member_id, guild_id); res.sendStatus(204); }); diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts b/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts
index e4308364..3f2975e6 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts
@@ -1,8 +1,7 @@ -import { getPermission, PermissionResolvable } from "@fosscord/util"; +import { getPermission, Member, PermissionResolvable } from "@fosscord/util"; import { Request, Response, Router } from "express"; import { check } from "lambert-server"; import { MemberNickChangeSchema } from "../../../../../schema/Member"; -import { changeNickname } from "../../../../../util/Member"; const router = Router(); @@ -17,7 +16,7 @@ router.patch("/", check(MemberNickChangeSchema), async (req: Request, res: Respo const perms = await getPermission(req.user_id, guild_id); perms.hasThrow(permissionString); - await changeNickname(member_id, guild_id, req.body.nick); + await Member.changeNickname(member_id, guild_id, req.body.nick); res.status(200).send(); }); diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
index fad0695e..cb9bad9a 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
@@ -1,6 +1,5 @@ -import { getPermission } from "@fosscord/util"; +import { getPermission, Member } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { addRole, removeRole } from "../../../../../../../util/Member"; const router = Router(); @@ -10,7 +9,7 @@ router.delete("/:member_id/roles/:role_id", async (req: Request, res: Response) const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_ROLES"); - await removeRole(member_id, guild_id, role_id); + await Member.removeRole(member_id, guild_id, role_id); res.sendStatus(204); }); @@ -20,7 +19,7 @@ router.put("/:member_id/roles/:role_id", async (req: Request, res: Response) => const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_ROLES"); - await addRole(member_id, guild_id, role_id); + await Member.addRole(member_id, guild_id, role_id); res.sendStatus(204); }); diff --git a/api/src/routes/guilds/#guild_id/members/index.ts b/api/src/routes/guilds/#guild_id/members/index.ts
index 70303436..0bfd71cb 100644 --- a/api/src/routes/guilds/#guild_id/members/index.ts +++ b/api/src/routes/guilds/#guild_id/members/index.ts
@@ -1,8 +1,7 @@ import { Request, Response, Router } from "express"; -import { GuildModel, MemberModel, toObject } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; +import { Guild, Member, PublicMemberProjection } from "@fosscord/util"; import { instanceOf, Length } from "../../../../util/instanceOf"; -import { PublicMemberProjection, isMember } from "../../../../util/Member"; +import { MoreThan } from "typeorm"; const router = Router(); @@ -10,8 +9,8 @@ const router = Router(); // TODO: send over websocket router.get("/", async (req: Request, res: Response) => { const { guild_id } = req.params; - const guild = await GuildModel.findOne({ id: guild_id }).exec(); - await isMember(req.user_id, guild_id); + const guild = await Guild.findOneOrFail({ id: guild_id }); + await Member.IsInGuildOrFail(req.user_id, guild_id); try { instanceOf({ $limit: new Length(Number, 1, 1000), $after: String }, req.query, { @@ -23,16 +22,17 @@ router.get("/", async (req: Request, res: Response) => { return res.status(400).json({ code: 50035, message: "Invalid Query", success: false, errors: error }); } - // @ts-ignore - if (!req.query.limit) req.query.limit = 1; - const { limit, after } = (<unknown>req.query) as { limit: number; after: string }; - const query = after ? { id: { $gt: after } } : {}; + const { limit, after } = (<unknown>req.query) as { limit?: number; after?: string }; + const query = after ? { id: MoreThan(after) } : {}; - var members = await MemberModel.find({ guild_id, ...query }, PublicMemberProjection) - .limit(limit) - .exec(); + const members = await Member.find({ + where: { guild_id, ...query }, + select: PublicMemberProjection, + take: limit || 1, + order: { id: "ASC" } + }); - return res.json(toObject(members)); + return res.json(members); }); export default router; diff --git a/api/src/routes/guilds/#guild_id/regions.ts b/api/src/routes/guilds/#guild_id/regions.ts
index 5ec649ee..212c9bcd 100644 --- a/api/src/routes/guilds/#guild_id/regions.ts +++ b/api/src/routes/guilds/#guild_id/regions.ts
@@ -1,10 +1,15 @@ -import { Config } from "@fosscord/util"; +import {Config, Guild, Member} from "@fosscord/util"; import { Request, Response, Router } from "express"; +import {getVoiceRegions} from "../../../util/Voice"; +import {getIpAdress} from "../../../util/ipAddress"; const router = Router(); router.get("/", async (req: Request, res: Response) => { - return res.json(Config.get().regions.available); + const { guild_id } = req.params; + const guild = await Guild.findOneOrFail({ id: guild_id }); + //TODO we should use an enum for guild's features and not hardcoded strings + return res.json(await getVoiceRegions(getIpAdress(req), guild.features.includes("VIP_REGIONS"))); }); export default router; diff --git a/api/src/routes/guilds/#guild_id/roles.ts b/api/src/routes/guilds/#guild_id/roles.ts
index f095c885..f6ac8caa 100644 --- a/api/src/routes/guilds/#guild_id/roles.ts +++ b/api/src/routes/guilds/#guild_id/roles.ts
@@ -1,12 +1,9 @@ import { Request, Response, Router } from "express"; import { - RoleModel, - GuildModel, + Role, getPermission, - toObject, - UserModel, Snowflake, - MemberModel, + Member, GuildRoleCreateEvent, GuildRoleUpdateEvent, GuildRoleDeleteEvent, @@ -16,40 +13,34 @@ import { HTTPError } from "lambert-server"; import { check } from "../../../util/instanceOf"; import { RoleModifySchema } from "../../../schema/Roles"; -import { getPublicUser } from "../../../util/User"; -import { isMember } from "../../../util/Member"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { const guild_id = req.params.guild_id; - await isMember(req.user_id, guild_id); + await Member.IsInGuildOrFail(req.user_id, guild_id); - const roles = await RoleModel.find({ guild_id: guild_id }).exec(); + const roles = await Role.find({ guild_id: guild_id }); - return res.json(toObject(roles)); + return res.json(roles); }); router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const body = req.body as RoleModifySchema; - const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec(); - const user = await UserModel.findOne({ id: req.user_id }).exec(); - const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_ROLES"); - if (!body.name) throw new HTTPError("You need to specify a name"); - const role = await new RoleModel({ + const role = await new Role({ ...body, id: Snowflake.generate(), guild_id: guild_id, managed: false, position: 0, tags: null, - permissions: body.permissions || 0n + permissions: String(perms.bitfield & (body.permissions || 0n)) }).save(); await emitEvent({ @@ -57,11 +48,11 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => guild_id, data: { guild_id, - role: toObject(role) + role: role } } as GuildRoleCreateEvent); - res.json(toObject(role)); + res.json(role); }); router.delete("/:role_id", async (req: Request, res: Response) => { @@ -72,19 +63,20 @@ router.delete("/:role_id", async (req: Request, res: Response) => { const permissions = await getPermission(req.user_id, guild_id); permissions.hasThrow("MANAGE_ROLES"); - await RoleModel.deleteOne({ - id: role_id, - guild_id: guild_id - }).exec(); - - await emitEvent({ - event: "GUILD_ROLE_DELETE", - guild_id, - data: { + await Promise.all([ + Role.delete({ + id: role_id, + guild_id: guild_id + }), + emitEvent({ + event: "GUILD_ROLE_DELETE", guild_id, - role_id - } - } as GuildRoleDeleteEvent); + data: { + guild_id, + role_id + } + } as GuildRoleDeleteEvent) + ]); res.sendStatus(204); }); @@ -96,32 +88,24 @@ router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Res const { role_id } = req.params; const body = req.body as RoleModifySchema; - const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec(); - const user = await UserModel.findOne({ id: req.user_id }).exec(); - const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_ROLES"); - const role = await RoleModel.findOneAndUpdate( - { - id: role_id, - guild_id: guild_id - }, - // @ts-ignore - body, - { new: true } - ).exec(); + const role = new Role({ ...body, id: role_id, guild_id, permissions: perms.bitfield & (body.permissions || 0n) }); - await emitEvent({ - event: "GUILD_ROLE_UPDATE", - guild_id, - data: { + await Promise.all([ + role.save(), + emitEvent({ + event: "GUILD_ROLE_UPDATE", guild_id, - role - } - } as GuildRoleUpdateEvent); - - res.json(toObject(role)); + data: { + guild_id, + role + } + } as GuildRoleUpdateEvent) + ]); + + res.json(role); }); export default router; diff --git a/api/src/routes/guilds/#guild_id/templates.ts b/api/src/routes/guilds/#guild_id/templates.ts
index e441ee12..a7613abf 100644 --- a/api/src/routes/guilds/#guild_id/templates.ts +++ b/api/src/routes/guilds/#guild_id/templates.ts
@@ -1,5 +1,5 @@ import { Request, Response, Router } from "express"; -import { TemplateModel, GuildModel, getPermission, toObject, UserModel, Snowflake } from "@fosscord/util"; +import { Guild, getPermission, Template } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { TemplateCreateSchema, TemplateModifySchema } from "../../../schema/Template"; import { check } from "../../../util/instanceOf"; @@ -7,43 +7,41 @@ import { generateCode } from "../../../util/String"; const router: Router = Router(); -const TemplateGuildProjection = { - name: true, - description: true, - region: true, - verification_level: true, - default_message_notifications: true, - explicit_content_filter: true, - preferred_locale: true, - afk_timeout: true, - roles: true, - channels: true, - afk_channel_id: true, - system_channel_id: true, - system_channel_flags: true, - icon_hash: true -}; +const TemplateGuildProjection: (keyof Guild)[] = [ + "name", + "description", + "region", + "verification_level", + "default_message_notifications", + "explicit_content_filter", + "preferred_locale", + "afk_timeout", + "roles", + // "channels", + "afk_channel_id", + "system_channel_id", + "system_channel_flags", + "icon" +]; router.get("/", async (req: Request, res: Response) => { const { guild_id } = req.params; - var templates = await TemplateModel.find({ source_guild_id: guild_id }).exec(); + var templates = await Template.find({ source_guild_id: guild_id }); - return res.json(toObject(templates)); + return res.json(templates); }); router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response) => { const { guild_id } = req.params; - const guild = await GuildModel.findOne({ id: guild_id }, TemplateGuildProjection).exec(); + const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_GUILD"); - const exists = await TemplateModel.findOne({ id: guild_id }) - .exec() - .catch((e) => {}); + const exists = await Template.findOneOrFail({ id: guild_id }).catch((e) => {}); if (exists) throw new HTTPError("Template already exists", 400); - const template = await new TemplateModel({ + const template = await new Template({ ...req.body, code: generateCode(), creator_id: req.user_id, @@ -53,7 +51,7 @@ router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response serialized_source_guild: guild }).save(); - res.json(toObject(template)).send(); + res.json(template); }); router.delete("/:code", async (req: Request, res: Response) => { @@ -63,41 +61,39 @@ router.delete("/:code", async (req: Request, res: Response) => { const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_GUILD"); - const template = await TemplateModel.findOneAndDelete({ + const template = await Template.delete({ code - }).exec(); + }); - res.send(toObject(template)); + res.json(template); }); router.put("/:code", async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const { code } = req.params; + // synchronizes the template + const { code, guild_id } = req.params; - const guild = await GuildModel.findOne({ id: guild_id }, TemplateGuildProjection).exec(); + const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_GUILD"); - const template = await TemplateModel.findOneAndUpdate({ code }, { serialized_source_guild: guild }, { new: true }).exec(); + const template = await new Template({ code, serialized_source_guild: guild }).save(); - res.json(toObject(template)).send(); + res.json(template); }); router.patch("/:code", check(TemplateModifySchema), async (req: Request, res: Response) => { + // updates the template description const { guild_id } = req.params; const { code } = req.params; + const { name, description } = req.body; const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_GUILD"); - const template = await TemplateModel.findOneAndUpdate( - { code }, - { name: req.body.name, description: req.body.description }, - { new: true } - ).exec(); + const template = await new Template({ code, name: name, description: description }).save(); - res.json(toObject(template)).send(); + res.json(template); }); export default router; diff --git a/api/src/routes/guilds/#guild_id/vanity-url.ts b/api/src/routes/guilds/#guild_id/vanity-url.ts
index 1e659d8d..58940b42 100644 --- a/api/src/routes/guilds/#guild_id/vanity-url.ts +++ b/api/src/routes/guilds/#guild_id/vanity-url.ts
@@ -1,8 +1,7 @@ -import { ChannelModel, ChannelType, getPermission, GuildModel, InviteModel, trimSpecial } from "@fosscord/util"; +import { Channel, ChannelType, getPermission, Guild, Invite, trimSpecial } from "@fosscord/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(); @@ -14,46 +13,37 @@ router.get("/", async (req: Request, res: Response) => { const permission = await getPermission(req.user_id, guild_id); permission.hasThrow("MANAGE_GUILD"); - const guild = await GuildModel.findOne({ id: guild_id }).exec(); - if (!guild.vanity_url_code) return res.json({ code: null }); - const { uses } = await InviteModel.findOne({ code: guild.vanity_url_code }).exec(); + const guild = await Guild.findOneOrFail({ where: { id: guild_id }, relations: ["vanity_url"] }); + if (!guild.vanity_url) return res.json({ code: null }); - return res.json({ code: guild.vanity_url_code, uses }); + return res.json({ code: guild.vanity_url_code, uses: guild.vanity_url.uses }); }); // 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 code = req.body.code.replace(InviteRegex); - const guild = await GuildModel.findOne({ id: guild_id }).exec(); - const permission = await getPermission(req.user_id, guild_id, undefined, { guild }); + await Invite.findOneOrFail({ code }); + + const guild = await Guild.findOneOrFail({ id: guild_id }); + 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); + const { id } = await Channel.findOneOrFail({ guild_id, type: ChannelType.GUILD_TEXT }); + guild.vanity_url_code = code; - await GuildModel.updateOne({ id: guild_id }, { vanity_url_code: code }).exec(); - const { id } = await ChannelModel.findOne({ guild_id, type: ChannelType.GUILD_TEXT }).exec(); - await InviteModel.updateOne( - { code: guild.vanity_url_code }, - { + Promise.all([ + guild.save(), + Invite.delete({ code: guild.vanity_url_code }), + new Invite({ code: code, uses: 0, created_at: new Date(), guild_id, channel_id: id - }, - { upsert: true } - ).exec(); + }).save() + ]); return res.json({ code: code }); }); diff --git a/api/src/routes/guilds/#guild_id/welcome_screen.ts b/api/src/routes/guilds/#guild_id/welcome_screen.ts
index c717042e..defbcd40 100644 --- a/api/src/routes/guilds/#guild_id/welcome_screen.ts +++ b/api/src/routes/guilds/#guild_id/welcome_screen.ts
@@ -1,47 +1,35 @@ import { Request, Response, Router } from "express"; -import { GuildModel, getPermission, toObject, Snowflake } from "@fosscord/util"; +import { Guild, getPermission, Snowflake, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { check } from "../../../util/instanceOf"; -import { isMember } from "../../../util/Member"; -import { GuildAddChannelToWelcomeScreenSchema } from "../../../schema/Guild"; -import { getPublicUser } from "../../../util/User"; +import { GuildUpdateWelcomeScreenSchema } from "../../../schema/Guild"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { const guild_id = req.params.guild_id; - const guild = await GuildModel.findOne({ id: guild_id }); + const guild = await Guild.findOneOrFail({ id: guild_id }); - await isMember(req.user_id, guild_id); + await Member.IsInGuildOrFail(req.user_id, guild_id); - res.json(toObject(guild.welcome_screen)); + res.json(guild.welcome_screen); }); -router.post("/", check(GuildAddChannelToWelcomeScreenSchema), async (req: Request, res: Response) => { +router.patch("/", check(GuildUpdateWelcomeScreenSchema), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; - const body = req.body as GuildAddChannelToWelcomeScreenSchema; + const body = req.body as GuildUpdateWelcomeScreenSchema; - const guild = await GuildModel.findOne({ id: guild_id }).exec(); - - var channelObject = { - ...body - }; + const guild = await Guild.findOneOrFail({ id: guild_id }); const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_GUILD"); if (!guild.welcome_screen.enabled) throw new HTTPError("Welcome screen disabled", 400); - if (guild.welcome_screen.welcome_channels.some((channel) => channel.channel_id === body.channel_id)) - throw new Error("Welcome Channel exists"); - - await GuildModel.findOneAndUpdate( - { - id: guild_id - }, - { $push: { "welcome_screen.welcome_channels": channelObject } } - ).exec(); + if (body.welcome_channels) guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid + if (body.description) guild.welcome_screen.description = body.description; + if (body.enabled != null) guild.welcome_screen.enabled = body.enabled; res.sendStatus(204); }); diff --git a/api/src/routes/guilds/#guild_id/widget.json.ts b/api/src/routes/guilds/#guild_id/widget.json.ts
index 8719bd85..ae1f0599 100644 --- a/api/src/routes/guilds/#guild_id/widget.json.ts +++ b/api/src/routes/guilds/#guild_id/widget.json.ts
@@ -1,5 +1,5 @@ import { Request, Response, Router } from "express"; -import { Config, Permissions, GuildModel, InviteModel, ChannelModel, MemberModel } from "@fosscord/util"; +import { Config, Permissions, Guild, Invite, Channel, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { random } from "../../../util/RandomInviteID"; @@ -17,11 +17,12 @@ const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { const { guild_id } = req.params; - const guild = await GuildModel.findOne({ id: guild_id }).exec(); + const guild = await Guild.findOneOrFail({ id: guild_id }); if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404); // Fetch existing widget invite for widget channel - var invite = await InviteModel.findOne({ channel_id: guild.widget_channel_id, inviter_id: { $type: 10 } }).exec(); + var invite = await Invite.findOne({ channel_id: guild.widget_channel_id }); + if (guild.widget_channel_id && !invite) { // Create invite for channel if none exists // TODO: Refactor invite create code to a shared function @@ -40,87 +41,29 @@ router.get("/", async (req: Request, res: Response) => { inviter_id: null }; - invite = await new InviteModel(body).save(); + invite = await new Invite(body).save(); } // Fetch voice channels, and the @everyone permissions object - let channels: any[] = []; - await ChannelModel.find({ guild_id: guild_id, type: 2 }, { permission_overwrites: { $elemMatch: { id: guild_id } } }) - .lean() - .select("id name position permission_overwrites") - .sort({ position: 1 }) - .cursor() - .eachAsync((doc) => { - // Only return channels where @everyone has the CONNECT permission - if ( - doc.permission_overwrites === undefined || - Permissions.channelPermission(doc.permission_overwrites, Permissions.FLAGS.CONNECT) === Permissions.FLAGS.CONNECT - ) { - channels.push({ - id: doc.id, - name: doc.name, - position: doc.position - }); - } - }); + const channels = [] as any[]; + + (await Channel.find({ where: { guild_id: guild_id, type: 2 }, order: { position: "ASC" } })).filter((doc) => { + // Only return channels where @everyone has the CONNECT permission + if ( + doc.permission_overwrites === undefined || + Permissions.channelPermission(doc.permission_overwrites, Permissions.FLAGS.CONNECT) === Permissions.FLAGS.CONNECT + ) { + channels.push({ + id: doc.id, + name: doc.name, + position: doc.position + }); + } + }); // Fetch members // TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file) - let members: any[] = []; - await MemberModel.find({ guild_id: guild_id }) - .lean() - .populate({ path: "user", select: { _id: 0, username: 1, avatar: 1, presence: 1 } }) - .select("id user nick deaf mute") - .cursor() - .eachAsync((doc) => { - const status = doc.user?.presence?.status || "offline"; - if (status == "offline") return; - - let item = {}; - - item = { - ...item, - id: null, // this is updated during the sort outside of the query - username: doc.nick || doc.user?.username, - discriminator: "0000", // intended (https://github.com/discord/discord-api-docs/issues/1287) - avatar: null, // intended, avatar_url below will return a unique guild + user url to the avatar - status: status - }; - - const activity = doc.user?.presence?.activities?.[0]; - if (activity) { - item = { - ...item, - game: { name: activity.name } - }; - } - - // TODO: If the member is in a voice channel, return extra widget details - // Extra fields returned include deaf, mute, self_deaf, self_mute, supress, and channel_id (voice channel connected to) - // Get this from VoiceState - - // TODO: Implement a widget-avatar endpoint on the CDN, and implement logic here to request it - // Get unique avatar url for guild user, cdn to serve the actual avatar image on this url - /* - const avatar = doc.user?.avatar; - if (avatar) { - const CDN_HOST = Config.get().cdn.endpoint || "http://localhost:3003"; - const avatar_url = "/widget-avatars/" + ; - item = { - ...item, - avatar_url: avatar_url - } - } - */ - - members.push(item); - }); - - // Sort members, and update ids (Unable to do under the mongoose query due to https://mongoosejs.com/docs/faq.html#populate_sort_order) - members = members.sort((first, second) => 0 - (first.username > second.username ? -1 : 1)); - members.forEach((x, i) => { - x.id = i; - }); + let members = await Member.find({ where: { guild_id: guild_id } }); // Construct object to respond with const data = { diff --git a/api/src/routes/guilds/#guild_id/widget.png.ts b/api/src/routes/guilds/#guild_id/widget.png.ts
index 80dc9f2b..89b31153 100644 --- a/api/src/routes/guilds/#guild_id/widget.png.ts +++ b/api/src/routes/guilds/#guild_id/widget.png.ts
@@ -1,5 +1,5 @@ import { Request, Response, Router } from "express"; -import { GuildModel } from "@fosscord/util"; +import { Guild } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import fs from "fs"; import path from "path"; @@ -13,7 +13,7 @@ const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { const { guild_id } = req.params; - const guild = await GuildModel.findOne({ id: guild_id }).exec(); + const guild = await Guild.findOneOrFail({ id: guild_id }); if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404); // Fetch guild information diff --git a/api/src/routes/guilds/#guild_id/widget.ts b/api/src/routes/guilds/#guild_id/widget.ts
index 85eed5e9..fcf71402 100644 --- a/api/src/routes/guilds/#guild_id/widget.ts +++ b/api/src/routes/guilds/#guild_id/widget.ts
@@ -1,5 +1,5 @@ import { Request, Response, Router } from "express"; -import { getPermission, GuildModel } from "@fosscord/util"; +import { getPermission, Guild } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { check } from "../../../util/instanceOf"; import { WidgetModifySchema } from "../../../schema/Widget"; @@ -13,7 +13,7 @@ router.get("/", async (req: Request, res: Response) => { const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_GUILD"); - const guild = await GuildModel.findOne({ id: guild_id }).exec(); + const guild = await Guild.findOneOrFail({ id: guild_id }); return res.json({ enabled: guild.widget_enabled || false, channel_id: guild.widget_channel_id || null }); }); @@ -26,7 +26,7 @@ router.patch("/", check(WidgetModifySchema), async (req: Request, res: Response) const perms = await getPermission(req.user_id, guild_id); perms.hasThrow("MANAGE_GUILD"); - await GuildModel.updateOne({ id: guild_id }, { widget_enabled: body.enabled, widget_channel_id: body.channel_id }).exec(); + await Guild.update({ id: guild_id }, { widget_enabled: body.enabled, widget_channel_id: body.channel_id }); // Widget invite for the widget_channel_id gets created as part of the /guilds/{guild.id}/widget.json request return res.json(body); diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts
index 92feed4e..e4157384 100644 --- a/api/src/routes/guilds/index.ts +++ b/api/src/routes/guilds/index.ts
@@ -1,11 +1,8 @@ import { Router, Request, Response } from "express"; -import { RoleModel, GuildModel, Snowflake, Guild, RoleDocument, Config } from "@fosscord/util"; +import { Role, Guild, Snowflake, Config, User, Member, Channel } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { check } from "./../../util/instanceOf"; import { GuildCreateSchema } from "../../schema/Guild"; -import { getPublicUser } from "../../util/User"; -import { addMember } from "../../util/Member"; -import { createChannel } from "../../util/Channel"; const router: Router = Router(); @@ -15,58 +12,43 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) = const body = req.body as GuildCreateSchema; const { maxGuilds } = Config.get().limits.user; - const user = await getPublicUser(req.user_id, { guilds: true }); - - if (user.guilds.length >= maxGuilds) { + const guild_count = await Member.count({ id: req.user_id }); + if (guild_count >= maxGuilds) { throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403); } const guild_id = Snowflake.generate(); - const guild: Guild = { - name: body.name, - region: Config.get().regions.default, - owner_id: req.user_id, - icon: undefined, - afk_channel_id: undefined, - afk_timeout: 300, - application_id: undefined, - banner: undefined, - default_message_notifications: 0, - description: undefined, - splash: undefined, - discovery_splash: undefined, - explicit_content_filter: 0, - features: [], - id: guild_id, - large: undefined, - max_members: 250000, - max_presences: 250000, - max_video_channel_users: 25, - presence_count: 0, - member_count: 0, // will automatically be increased by addMember() - mfa_level: 0, - preferred_locale: "en-US", - premium_subscription_count: 0, - premium_tier: 0, - public_updates_channel_id: undefined, - rules_channel_id: undefined, - system_channel_flags: 0, - system_channel_id: undefined, - unavailable: false, - vanity_url_code: undefined, - verification_level: 0, - welcome_screen: { - enabled: false, - description: "No description", - welcome_channels: [] - }, - widget_channel_id: undefined, - widget_enabled: false - }; - const [guild_doc, role] = await Promise.all([ - new GuildModel(guild).save(), - new RoleModel({ + const [guild, role] = await Promise.all([ + Guild.insert({ + name: body.name, + region: Config.get().regions.default, + owner_id: req.user_id, + afk_timeout: 300, + default_message_notifications: 0, + explicit_content_filter: 0, + features: [], + id: guild_id, + max_members: 250000, + max_presences: 250000, + max_video_channel_users: 25, + presence_count: 0, + member_count: 0, // will automatically be increased by addMember() + mfa_level: 0, + preferred_locale: "en-US", + premium_subscription_count: 0, + premium_tier: 0, + system_channel_flags: 0, + unavailable: false, + verification_level: 0, + welcome_screen: { + enabled: false, + description: "No description", + welcome_channels: [] + }, + widget_enabled: false + }), + Role.insert({ id: guild_id, guild_id: guild_id, color: 0, @@ -74,10 +56,9 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) = managed: false, mentionable: false, name: "@everyone", - permissions: 2251804225n, - position: 0, - tags: null - }).save() + permissions: String("2251804225"), + position: 0 + }) ]); if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general" }]; @@ -94,16 +75,21 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) = body.channels?.map((x) => { var id = ids.get(x.id) || Snowflake.generate(); - // TODO: should we abort if parent_id is a category? (or not to allow sub category channels) + // TODO: should we abort if parent_id is a category? (to disallow sub category channels) var parent_id = ids.get(x.parent_id); - return createChannel({ ...x, guild_id, id, parent_id }, req.user_id, { keepId: true, skipExistsCheck: true }); + return Channel.createChannel({ ...x, guild_id, id, parent_id }, req.user_id, { + keepId: true, + skipExistsCheck: true, + skipPermissionCheck: true, + skipEventEmit: true + }); }) ); - await addMember(req.user_id, guild_id); + await Member.addToGuild(req.user_id, guild_id); - res.status(201).json({ id: guild.id }); + res.status(201).json({ id: guild_id }); }); export default router; diff --git a/api/src/routes/guilds/templates/index.ts b/api/src/routes/guilds/templates/index.ts
index 7fed3c5d..7a8ac886 100644 --- a/api/src/routes/guilds/templates/index.ts +++ b/api/src/routes/guilds/templates/index.ts
@@ -1,18 +1,16 @@ import { Request, Response, Router } from "express"; const router: Router = Router(); -import { TemplateModel, GuildModel, toObject, UserModel, RoleModel, Snowflake, Guild, Config } from "@fosscord/util"; +import { Template, Guild, Role, Snowflake, Config, User, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { GuildTemplateCreateSchema } from "../../../schema/Guild"; -import { getPublicUser } from "../../../util/User"; import { check } from "../../../util/instanceOf"; -import { addMember } from "../../../util/Member"; router.get("/:code", async (req: Request, res: Response) => { const { code } = req.params; - const template = await TemplateModel.findOne({ code: code }).exec(); + const template = await Template.findOneOrFail({ code: code }); - res.json(toObject(template)).send(); + res.json(template); }); router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res: Response) => { @@ -20,26 +18,24 @@ router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res const body = req.body as GuildTemplateCreateSchema; const { maxGuilds } = Config.get().limits.user; - const user = await getPublicUser(req.user_id, { guilds: true }); - if (user.guilds.length >= maxGuilds) { + const guild_count = await Member.count({ id: req.user_id }); + if (guild_count >= maxGuilds) { throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403); } - const template = await TemplateModel.findOne({ code: code }).exec(); + const template = await Template.findOneOrFail({ code: code }); const guild_id = Snowflake.generate(); - const guild: Guild = { - ...body, - ...template.serialized_source_guild, - id: guild_id, - owner_id: req.user_id - }; - - const [guild_doc, role] = await Promise.all([ - new GuildModel(guild).save(), - new RoleModel({ + const [guild, role] = await Promise.all([ + new Guild({ + ...body, + ...template.serialized_source_guild, + id: guild_id, + owner_id: req.user_id + }).save(), + new Role({ id: guild_id, guild_id: guild_id, color: 0, @@ -53,7 +49,7 @@ router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res }).save() ]); - await addMember(req.user_id, guild_id, { guild: guild_doc }); + await Member.addToGuild(req.user_id, guild_id); res.status(201).json({ id: guild.id }); }); diff --git a/api/src/routes/invites/index.ts b/api/src/routes/invites/index.ts
index e7543dbb..23fa3aec 100644 --- a/api/src/routes/invites/index.ts +++ b/api/src/routes/invites/index.ts
@@ -1,48 +1,41 @@ import { Router, Request, Response } from "express"; -import { getPermission, GuildModel, InviteModel, toObject } from "@fosscord/util"; +import { getPermission, Guild, Invite, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { addMember } from "../../util/Member"; const router: Router = Router(); router.get("/:code", async (req: Request, res: Response) => { const { code } = req.params; - const invite = await InviteModel.findOne({ code }).exec(); - if (!invite) throw new HTTPError("Unknown Invite", 404); + const invite = await Invite.findOneOrFail({ code }); - res.status(200).send(toObject(invite)); + res.status(200).send(invite); }); router.post("/:code", async (req: Request, res: Response) => { const { code } = req.params; - const invite = await InviteModel.findOneAndUpdate({ code }, { $inc: { uses: 1 } }, { new: true }).exec(); - if (!invite) throw new HTTPError("Unknown Invite", 404); - if (invite.uses >= invite.max_uses) await InviteModel.deleteOne({ code }); + const invite = await Invite.findOneOrFail({ code }); + if (invite.uses++ >= invite.max_uses) await Invite.delete({ code }); + else await invite.save(); - await addMember(req.user_id, invite.guild_id); + await Member.addToGuild(req.user_id, invite.guild_id); - res.status(200).send(toObject(invite)); + res.status(200).send(invite); }); router.delete("/:code", async (req: Request, res: Response) => { const { code } = req.params; - const invite = await InviteModel.findOne({ code }).exec(); + const invite = await Invite.findOneOrFail({ code }); const { guild_id, channel_id } = invite; - const guild = await GuildModel.findOne({ id: guild_id }).exec(); - const permission = await getPermission(req.user_id, guild_id, channel_id, { guild }); + const permission = await getPermission(req.user_id, guild_id, channel_id); if (!permission.has("MANAGE_GUILD") && !permission.has("MANAGE_CHANNELS")) throw new HTTPError("You missing the MANAGE_GUILD or MANAGE_CHANNELS permission", 401); - await InviteModel.deleteOne({ code }).exec(); + await Promise.all([Invite.delete({ code }), Guild.update({ vanity_url_code: code }, { vanity_url_code: undefined })]); - await GuildModel.updateOne({ vanity_url_code: code }, { $unset: { vanity_url_code: 1 } }) - .exec() - .catch((e) => {}); - - res.status(200).send({ invite: toObject(invite) }); + res.json({ invite: invite }); }); export default router; diff --git a/api/src/routes/users/#id/index.ts b/api/src/routes/users/#id/index.ts
index a2ad3ae6..3841756b 100644 --- a/api/src/routes/users/#id/index.ts +++ b/api/src/routes/users/#id/index.ts
@@ -1,13 +1,12 @@ import { Router, Request, Response } from "express"; -import { getPublicUser } from "../../../util/User"; -import { HTTPError } from "lambert-server"; +import { User } from "../../../../../util/dist"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { const { id } = req.params; - res.json(await getPublicUser(id)); + res.json(await User.getPublicUser(id)); }); export default router; diff --git a/api/src/routes/users/#id/profile.ts b/api/src/routes/users/#id/profile.ts
index 4b4b9439..afccfed5 100644 --- a/api/src/routes/users/#id/profile.ts +++ b/api/src/routes/users/#id/profile.ts
@@ -1,27 +1,27 @@ import { Router, Request, Response } from "express"; -import { getPublicUser } from "../../../util/User"; +import { User } from "../../../../../util/dist"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { - const user = await getPublicUser(req.params.id, { user_data: true }) + const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] }); - res.json({ - connected_accounts: user.user_data.connected_accounts, - premium_guild_since: null, // TODO - premium_since: null, // TODO - user: { - username: user.username, - discriminator: user.discriminator, - id: user.id, - 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, - } - }); + res.json({ + connected_accounts: user.connected_accounts, + premium_guild_since: null, // TODO + premium_since: null, // TODO + user: { + username: user.username, + discriminator: user.discriminator, + id: user.id, + 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 + } + }); }); export default router; diff --git a/api/src/routes/users/@me/channels.ts b/api/src/routes/users/@me/channels.ts
index db9f8832..880e09c1 100644 --- a/api/src/routes/users/@me/channels.ts +++ b/api/src/routes/users/@me/channels.ts
@@ -1,27 +1,18 @@ import { Router, Request, Response } from "express"; -import { - ChannelModel, - ChannelCreateEvent, - toObject, - ChannelType, - Snowflake, - trimSpecial, - Channel, - DMChannel, - UserModel, - emitEvent -} from "@fosscord/util"; +import { Channel, ChannelCreateEvent, ChannelType, Snowflake, trimSpecial, User, emitEvent } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { DmChannelCreateSchema } from "../../../schema/Channel"; import { check } from "../../../util/instanceOf"; +import { In } from "typeorm"; +import { Recipient } from "../../../../../util/dist/entities/Recipient"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { - var channels = await ChannelModel.find({ recipient_ids: req.user_id }).exec(); + const recipients = await Recipient.find({ where: { id: req.user_id }, relations: ["channel"] }); - res.json(toObject(channels)); + res.json(recipients.map((x) => x.channel)); }); router.post("/", check(DmChannelCreateSchema), async (req: Request, res: Response) => { @@ -29,26 +20,27 @@ router.post("/", check(DmChannelCreateSchema), async (req: Request, res: Respons body.recipients = body.recipients.filter((x) => x !== req.user_id).unique(); - if (!(await Promise.all(body.recipients.map((x) => UserModel.exists({ id: x })))).every((x) => x)) { - throw new HTTPError("Recipient not found"); + const recipients = await User.find({ id: In(body.recipients) }); + + if (recipients.length !== body.recipients.length) { + throw new HTTPError("Recipient/s not found"); } const type = body.recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM; const name = trimSpecial(body.name); - const channel = await new ChannelModel({ + const channel = await new Channel({ name, type, owner_id: req.user_id, - id: Snowflake.generate(), created_at: new Date(), last_message_id: null, - recipient_ids: [...body.recipients, req.user_id] + recipients: [...body.recipients.map((x) => new Recipient({ id: x })), new Recipient({ id: req.user_id })] }).save(); - await emitEvent({ event: "CHANNEL_CREATE", data: toObject(channel), user_id: req.user_id } as ChannelCreateEvent); + await emitEvent({ event: "CHANNEL_CREATE", data: channel, user_id: req.user_id } as ChannelCreateEvent); - res.json(toObject(channel)); + res.json(channel); }); export default router; diff --git a/api/src/routes/users/@me/delete.ts b/api/src/routes/users/@me/delete.ts
index f863237d..6bfe0b93 100644 --- a/api/src/routes/users/@me/delete.ts +++ b/api/src/routes/users/@me/delete.ts
@@ -1,16 +1,23 @@ import { Router, Request, Response } from "express"; -import { GuildModel, MemberModel, UserModel } from "@fosscord/util"; +import { Guild, Member, User } from "@fosscord/util"; import bcrypt from "bcrypt"; const router = Router(); router.post("/", async (req: Request, res: Response) => { - const user = await UserModel.findOne({ id: req.user_id }).exec(); //User object + const user = await User.findOneOrFail({ id: req.user_id }); //User object + let correctpass = true; + + if (user.data.hash) { + // guest accounts can delete accounts without password + correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/ + } + + // TODO: decrement guild member count - let correctpass = await bcrypt.compare(req.body.password, user!.user_data.hash); //Not sure if user typed right password :/ if (correctpass) { await Promise.all([ - UserModel.deleteOne({ id: req.user_id }).exec(), //Yeetus user deletus - MemberModel.deleteMany({ id: req.user_id }).exec() + User.delete({ id: req.user_id }), //Yeetus user deletus + Member.delete({ id: req.user_id }) ]); res.sendStatus(204); diff --git a/api/src/routes/users/@me/disable.ts b/api/src/routes/users/@me/disable.ts
index 2d3a9850..7b8a130c 100644 --- a/api/src/routes/users/@me/disable.ts +++ b/api/src/routes/users/@me/disable.ts
@@ -1,15 +1,20 @@ -import { UserModel } from "@fosscord/util"; +import { User } from "@fosscord/util"; import { Router, Response, Request } from "express"; import bcrypt from "bcrypt"; const router = Router(); router.post("/", async (req: Request, res: Response) => { - const user = await UserModel.findOne({ id: req.user_id }).exec(); //User object + const user = await User.findOneOrFail({ id: req.user_id }); //User object + let correctpass = true; + + if (user.data.hash) { + // guest accounts can delete accounts without password + correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/ + } - 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(); + await User.update({ id: req.user_id }, { disabled: true }); res.sendStatus(204); } else { diff --git a/api/src/routes/users/@me/guilds.ts b/api/src/routes/users/@me/guilds.ts
index a9b53b75..fb88281b 100644 --- a/api/src/routes/users/@me/guilds.ts +++ b/api/src/routes/users/@me/guilds.ts
@@ -1,34 +1,26 @@ import { Router, Request, Response } from "express"; -import { GuildModel, MemberModel, UserModel, GuildDeleteEvent, GuildMemberRemoveEvent, toObject, emitEvent } from "@fosscord/util"; +import { Guild, Member, User, GuildDeleteEvent, GuildMemberRemoveEvent, emitEvent } from "@fosscord/util"; import { HTTPError } from "lambert-server"; - -import { getPublicUser } from "../../../util/User"; +import { In } from "typeorm"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { - const user = await UserModel.findOne({ id: req.user_id }, { guilds: true }).exec(); - if (!user) throw new HTTPError("User not found", 404); - - var guildIDs = user.guilds || []; - var guild = await GuildModel.find({ id: { $in: guildIDs } }) - .populate({ path: "joined_at", match: { id: req.user_id } }) - .exec(); + const members = await Member.find({ relations: ["guild"], where: { id: req.user_id } }); - res.json(toObject(guild)); + res.json(members.map((x) => x.guild)); }); // user send to leave a certain guild router.delete("/:id", async (req: Request, res: Response) => { const guild_id = req.params.id; - const guild = await GuildModel.findOne({ id: guild_id }, { guild_id: true }).exec(); + const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] }); if (!guild) throw new HTTPError("Guild doesn't exist", 404); if (guild.owner_id === req.user_id) throw new HTTPError("You can't leave your own guild", 400); await Promise.all([ - MemberModel.deleteOne({ id: req.user_id, guild_id: guild_id }).exec(), - UserModel.updateOne({ id: req.user_id }, { $pull: { guilds: guild_id } }).exec(), + Member.delete({ id: req.user_id, guild_id: guild_id }), emitEvent({ event: "GUILD_DELETE", data: { @@ -38,7 +30,7 @@ router.delete("/:id", async (req: Request, res: Response) => { } as GuildDeleteEvent) ]); - const user = await getPublicUser(req.user_id); + const user = await User.getPublicUser(req.user_id); await emitEvent({ event: "GUILD_MEMBER_REMOVE", diff --git a/api/src/routes/users/@me/index.ts b/api/src/routes/users/@me/index.ts
index 6ebc6634..d5a5723c 100644 --- a/api/src/routes/users/@me/index.ts +++ b/api/src/routes/users/@me/index.ts
@@ -1,6 +1,5 @@ import { Router, Request, Response } from "express"; -import { UserModel, toObject, PublicUserProjection } from "@fosscord/util"; -import { getPublicUser } from "../../../util/User"; +import { User } from "@fosscord/util"; import { UserModifySchema } from "../../../schema/User"; import { check } from "../../../util/instanceOf"; import { handleFile } from "../../../util/cdn"; @@ -8,29 +7,29 @@ import { handleFile } from "../../../util/cdn"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { - res.json(await getPublicUser(req.user_id)); + res.json(await User.getPublicUser(req.user_id)); }); -const UserUpdateProjection = { - accent_color: true, - avatar: true, - banner: true, - bio: true, - bot: true, - discriminator: true, - email: true, - flags: true, - id: true, - locale: true, - mfa_enabled: true, - nsfw_alllowed: true, - phone: true, - public_flags: true, - purchased_flags: true, - // token: true, // this isn't saved in the db and needs to be set manually - username: true, - verified: true -}; +const UserUpdateProjection = [ + "accent_color", + "avatar", + "banner", + "bio", + "bot", + "discriminator", + "email", + "flags", + "id", + "locale", + "mfa_enabled", + "nsfw_alllowed", + "phone", + "public_flags", + "purchased_flags", + // "token", // this isn't saved in the db and needs to be set manually + "username", + "verified" +]; router.patch("/", check(UserModifySchema), async (req: Request, res: Response) => { const body = req.body as UserModifySchema; @@ -38,10 +37,10 @@ router.patch("/", check(UserModifySchema), async (req: Request, res: Response) = 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: UserUpdateProjection, new: true }).exec(); + const user = await new User({ ...body, id: req.user_id }).save(); // TODO: dispatch user update event - res.json(toObject(user)); + res.json(user); }); export default router; diff --git a/api/src/routes/users/@me/profile.ts b/api/src/routes/users/@me/profile.ts
index b67d1964..5ba03c68 100644 --- a/api/src/routes/users/@me/profile.ts +++ b/api/src/routes/users/@me/profile.ts
@@ -1,27 +1,27 @@ import { Router, Request, Response } from "express"; -import { getPublicUser } from "../../../util/User"; +import { User } from "../../../../../util/dist"; const router: Router = Router(); router.get("/", async (req: Request, res: Response) => { - const user = await getPublicUser(req.user_id, { user_data: true }) + const user = await User.getPublicUser(req.user_id, { relations: ["connected_accounts"] }); - res.json({ - connected_accounts: user.user_data.connected_accounts, - premium_guild_since: null, // TODO - premium_since: null, // TODO - user: { - username: user.username, - discriminator: user.discriminator, - id: user.id, - public_flags: user.public_flags, - avatar: user.avatar, - accent_color: user.accent_color, - banner: user.banner, - bio: user.bio, - bot: user.bot, - } - }); + res.json({ + connected_accounts: user.connected_accounts, + premium_guild_since: null, // TODO + premium_since: null, // TODO + user: { + username: user.username, + discriminator: user.discriminator, + id: user.id, + public_flags: user.public_flags, + avatar: user.avatar, + accent_color: user.accent_color, + banner: user.banner, + bio: user.bio, + bot: user.bot + } + }); }); export default router; diff --git a/api/src/routes/users/@me/relationships.ts b/api/src/routes/users/@me/relationships.ts
index 642ee5f9..0b864d88 100644 --- a/api/src/routes/users/@me/relationships.ts +++ b/api/src/routes/users/@me/relationships.ts
@@ -1,12 +1,11 @@ import { RelationshipAddEvent, - UserModel, + User, PublicUserProjection, - toObject, RelationshipType, RelationshipRemoveEvent, - UserDocument, - emitEvent + emitEvent, + Relationship } from "@fosscord/util"; import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; @@ -15,40 +14,36 @@ import { check, Length } from "../../../util/instanceOf"; const router = Router(); -const userProjection = { "user_data.relationships": true, ...PublicUserProjection }; +const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection]; 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(); + const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["relationships"] }); - return res.json(toObject(user.user_data.relationships)); + return res.json(user.relationships); }); -async function addRelationship(req: Request, res: Response, friend: UserDocument, type: RelationshipType) { +async function updateRelationship(req: Request, res: Response, friend: User, type: RelationshipType) { const id = friend.id; if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend"); - const user = await UserModel.findOne({ id: req.user_id }, userProjection).exec(); - const newUserRelationships = [...user.user_data.relationships]; - const newFriendRelationships = [...friend.user_data.relationships]; + const user = await User.findOneOrFail({ id: req.user_id }, { relations: ["relationships"], select: userProjection }); - var relationship = newUserRelationships.find((x) => x.id === id); - const friendRequest = newFriendRelationships.find((x) => x.id === req.user_id); + var relationship = user.relationships.find((x) => x.id === id); + const friendRequest = friend.relationships.find((x) => x.id === req.user_id); 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 }; - newUserRelationships.push(relationship); + relationship = new Relationship({ id, type: RelationshipType.blocked }); + user.relationships.push(relationship); } if (friendRequest && friendRequest.type !== RelationshipType.blocked) { - newFriendRelationships.remove(friendRequest); + friend.relationships.remove(friendRequest); await Promise.all([ - UserModel.updateOne({ id: friend.id }, { "user_data.relationships": newFriendRelationships }).exec(), + user.save(), emitEvent({ event: "RELATIONSHIP_REMOVE", data: friendRequest, @@ -58,12 +53,12 @@ async function addRelationship(req: Request, res: Response, friend: UserDocument } await Promise.all([ - UserModel.updateOne({ id: req.user_id }, { "user_data.relationships": newUserRelationships }).exec(), + user.save(), emitEvent({ event: "RELATIONSHIP_ADD", data: { - ...toObject(relationship), - user: { ...toObject(friend), user_data: undefined } + ...relationship, + user: { ...friend } }, user_id: req.user_id } as RelationshipAddEvent) @@ -72,41 +67,40 @@ async function addRelationship(req: Request, res: Response, friend: UserDocument return res.sendStatus(204); } - var incoming_relationship = { id: req.user_id, nickname: undefined, type: RelationshipType.incoming }; - var outgoing_relationship = { id, nickname: undefined, type: RelationshipType.outgoing }; + var incoming_relationship = new Relationship({ nickname: undefined, type: RelationshipType.incoming, id: req.user_id }); + var outgoing_relationship = new Relationship({ nickname: undefined, type: RelationshipType.outgoing, id }); if (friendRequest) { if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); // accept friend request - // @ts-ignore incoming_relationship = friendRequest; incoming_relationship.type = RelationshipType.friends; outgoing_relationship.type = RelationshipType.friends; - } else newFriendRelationships.push(incoming_relationship); + } else friend.relationships.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 newUserRelationships.push(outgoing_relationship); + } else user.relationships.push(outgoing_relationship); await Promise.all([ - UserModel.updateOne({ id: req.user_id }, { "user_data.relationships": newUserRelationships }).exec(), - UserModel.updateOne({ id: friend.id }, { "user_data.relationships": newFriendRelationships }).exec(), + user.save(), + friend.save(), emitEvent({ event: "RELATIONSHIP_ADD", data: { ...outgoing_relationship, - user: { ...toObject(friend), user_data: undefined } + user: { ...friend } }, user_id: req.user_id } as RelationshipAddEvent), emitEvent({ event: "RELATIONSHIP_ADD", data: { - ...toObject(incoming_relationship), + ...incoming_relationship, should_notify: true, - user: { ...toObject(user), user_data: undefined } + user: { ...user } }, user_id: id } as RelationshipAddEvent) @@ -116,14 +110,23 @@ async function addRelationship(req: Request, res: Response, friend: UserDocument } 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); + return await updateRelationship( + req, + res, + await User.findOneOrFail({ id: req.params.id }, { relations: ["relationships"], select: userProjection }), + req.body.type + ); }); router.post("/", check({ discriminator: String, username: String }), async (req: Request, res: Response) => { - return await addRelationship( + return await updateRelationship( req, res, - await UserModel.findOne(req.body as { discriminator: string; username: string }).exec(), + await User.findOneOrFail({ + relations: ["relationships"], + select: userProjection, + where: req.body as { discriminator: string; username: string } + }), req.body.type ); }); @@ -132,17 +135,15 @@ router.delete("/:id", async (req: Request, res: Response) => { const { id } = req.params; if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend"); - const user = await UserModel.findOne({ id: req.user_id }).exec(); - if (!user) throw new HTTPError("Invalid token", 400); + const user = await User.findOneOrFail({ id: req.user_id }, { select: userProjection, relations: ["relationships"] }); + const friend = await User.findOneOrFail({ id: id }, { select: userProjection, relations: ["relationships"] }); - const friend = await UserModel.findOne({ id }, userProjection).exec(); - if (!friend) throw new HTTPError("User not found", 404); + const relationship = user.relationships.find((x) => x.id === id); + const friendRequest = friend.relationships.find((x) => x.id === req.user_id); - const relationship = user.user_data.relationships.find((x) => x.id === id); - const friendRequest = friend.user_data.relationships.find((x) => x.id === req.user_id); if (relationship?.type === RelationshipType.blocked) { // unblock user - user.user_data.relationships.remove(relationship); + user.relationships.remove(relationship); await Promise.all([ user.save(), @@ -153,8 +154,8 @@ router.delete("/:id", async (req: Request, res: Response) => { if (!relationship || !friendRequest) throw new HTTPError("You are not friends with the user", 404); if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); - user.user_data.relationships.remove(relationship); - friend.user_data.relationships.remove(friendRequest); + user.relationships.remove(relationship); + friend.relationships.remove(friendRequest); await Promise.all([ user.save(), diff --git a/api/src/routes/users/@me/settings.ts b/api/src/routes/users/@me/settings.ts
index 5664fc2f..90ee6372 100644 --- a/api/src/routes/users/@me/settings.ts +++ b/api/src/routes/users/@me/settings.ts
@@ -1,5 +1,5 @@ import { Router, Response, Request } from "express"; -import { UserModel, UserSettings } from "@fosscord/util"; +import { User, UserSettings } from "@fosscord/util"; import { check } from "../../../util/instanceOf"; import { UserSettingsSchema } from "../../../schema/User"; @@ -8,7 +8,8 @@ const router = Router(); router.patch("/", check(UserSettingsSchema), async (req: Request, res: Response) => { const body = req.body as UserSettings; - await UserModel.updateOne({ id: req.user_id }, body).exec(); + // only users can update user settings + await User.update({ id: req.user_id, bot: false }, { settings: body }); res.sendStatus(204); }); diff --git a/api/src/routes/voice/regions.ts b/api/src/routes/voice/regions.ts new file mode 100644
index 00000000..812aa8f6 --- /dev/null +++ b/api/src/routes/voice/regions.ts
@@ -0,0 +1,11 @@ +import { Router, Request, Response } from "express"; +import {getIpAdress} from "../../util/ipAddress"; +import {getVoiceRegions} from "../../util/Voice"; + +const router: Router = Router(); + +router.get("/", async (req: Request, res: Response) => { + res.json(await getVoiceRegions(getIpAdress(req), true))//vip true? +}); + +export default router; diff --git a/api/src/schema/Guild.ts b/api/src/schema/Guild.ts
index e5971baf..3e98fe76 100644 --- a/api/src/schema/Guild.ts +++ b/api/src/schema/Guild.ts
@@ -1,4 +1,4 @@ -import { ChannelSchema, GuildChannel } from "@fosscord/util"; +import { Channel } from "@fosscord/util"; import { Length } from "../util/instanceOf"; import { ChannelModifySchema } from "./Channel"; @@ -33,7 +33,7 @@ export const GuildUpdateSchema = { $icon: String, $verification_level: Number, $default_message_notifications: Number, - $system_channel_flags: Number, + $system_channel_flags: String, $system_channel_id: String, $explicit_content_filter: Number, $public_updates_channel_id: String, @@ -59,54 +59,6 @@ export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> { preferred_locale?: string; } -export const GuildGetSchema = { - id: true, - name: true, - icon: true, - splash: true, - discovery_splash: true, - owner: true, - owner_id: true, - permissions: true, - region: true, - afk_channel_id: true, - afk_timeout: true, - widget_enabled: true, - widget_channel_id: true, - verification_level: true, - default_message_notifications: true, - explicit_content_filter: true, - roles: true, - emojis: true, - features: true, - mfa_level: true, - application_id: true, - system_channel_id: true, - system_channel_flags: true, - rules_channel_id: true, - joined_at: true, - // large: true, - // unavailable: true, - member_count: true, - // voice_states: true, - // members: true, - // channels: true, - // presences: true, - max_presences: true, - max_members: true, - vanity_url_code: true, - description: true, - banner: true, - premium_tier: true, - premium_subscription_count: true, - preferred_locale: true, - public_updates_channel_id: true, - max_video_channel_users: true, - approximate_member_count: true, - approximate_presence_count: true - // welcome_screen: true, -}; - export const GuildTemplateCreateSchema = { name: String, $avatar: String @@ -117,16 +69,26 @@ export interface GuildTemplateCreateSchema { avatar?: string; } -export const GuildAddChannelToWelcomeScreenSchema = { - channel_id: String, - description: String, - $emoji_id: String, - emoji_name: String +export const GuildUpdateWelcomeScreenSchema = { + $welcome_channels: [ + { + channel_id: String, + description: String, + $emoji_id: String, + emoji_name: String + } + ], + $enabled: Boolean, + $description: new Length(String, 0, 140) }; -export interface GuildAddChannelToWelcomeScreenSchema { - channel_id: string; - description: string; - emoji_id?: string; - emoji_name: string; +export interface GuildUpdateWelcomeScreenSchema { + welcome_channels?: { + channel_id: string; + description: string; + emoji_id?: string; + emoji_name: string; + }[]; + enabled?: boolean; + description?: string; } diff --git a/api/src/schema/Message.ts b/api/src/schema/Message.ts
index 2dd54f0c..742542df 100644 --- a/api/src/schema/Message.ts +++ b/api/src/schema/Message.ts
@@ -1,11 +1,17 @@ -import { Embed, EmbedImage } from "@fosscord/util"; +import { Embed } from "@fosscord/util"; import { Length } from "../util/instanceOf"; +export const EmbedImage = { + $url: String, + $width: Number, + $height: Number +}; + export const MessageCreateSchema = { $content: new Length(String, 0, 2000), $nonce: String, $tts: Boolean, - $flags: BigInt, + $flags: String, $embed: { $title: new Length(String, 0, 256), //title of embed $type: String, // type of embed (always "rich" for webhook embeds) @@ -63,7 +69,7 @@ export interface MessageCreateSchema { content?: string; nonce?: string; tts?: boolean; - flags?: bigint; + flags?: string; embed?: Embed & { timestamp?: string }; allowed_mentions?: { parse?: string[]; @@ -75,7 +81,7 @@ export interface MessageCreateSchema { message_id: string; channel_id: string; guild_id?: string; - fail_if_not_exists: boolean; + fail_if_not_exists?: boolean; }; payload_json?: string; file?: any; diff --git a/api/src/schema/Roles.ts b/api/src/schema/Roles.ts
index f662e61b..1e5f560f 100644 --- a/api/src/schema/Roles.ts +++ b/api/src/schema/Roles.ts
@@ -9,7 +9,7 @@ export const RoleModifySchema = { export interface RoleModifySchema { name?: string; - permissions?: BigInt; + permissions?: bigint; color?: number; hoist?: boolean; // whether the role should be displayed separately in the sidebar mentionable?: boolean; // whether the role should be mentionable diff --git a/api/src/test/mongo_test.ts b/api/src/test/mongo_test.ts deleted file mode 100644
index 44b04c5b..00000000 --- a/api/src/test/mongo_test.ts +++ /dev/null
@@ -1,40 +0,0 @@ -import mongoose, { Schema, Types } from "mongoose"; -require("mongoose-long")(mongoose); - -const userSchema = new Schema({ - id: String, -}); - -const messageSchema = new Schema({ - id: String, - content: String, -}); -const message = mongoose.model("message", messageSchema, "messages"); -const user = mongoose.model("user", userSchema, "users"); - -messageSchema.virtual("u", { - ref: user, - localField: "id", - foreignField: "id", - justOne: true, -}); - -messageSchema.set("toObject", { virtuals: true }); -messageSchema.set("toJSON", { virtuals: true }); - -async function main() { - const conn = await mongoose.connect("mongodb://localhost:27017/lambert?readPreference=secondaryPreferred", { - useNewUrlParser: true, - useUnifiedTopology: false, - }); - console.log("connected"); - - // const u = await new user({ name: "test" }).save(); - // await new message({ user: u._id, content: "test" }).save(); - - const test = await message.findOne({}).populate("u").exec(); - // @ts-ignore - console.log(test?.toJSON()); -} - -main(); diff --git a/api/src/test/server_benchmark.ts b/api/src/test/server_benchmark.ts deleted file mode 100644
index c582ee89..00000000 --- a/api/src/test/server_benchmark.ts +++ /dev/null
@@ -1,39 +0,0 @@ -// @ts-nocheck -import "missing-native-js-functions"; -import { config } from "dotenv"; -config(); -import { DiscordServer } from "../Server"; -import fetch from "node-fetch"; -import { promises } from "fs"; -const count = 100; - -async function main() { - const server = new DiscordServer({ port: 3000 }); - await server.start(); - - const tasks = []; - for (let i = 0; i < count; i++) { - tasks.push(test()); - } - - await Promise.all(tasks); - - console.log("logging in 5secs"); - setTimeout(async () => { - await test(); - - process.exit(); - }, 5000); -} -main(); - -async function test() { - const res = await fetch("http://localhost:3000/api/v8/guilds/813524615463698433/members/813524464300982272", { - headers: { - authorization: - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjgxMzUyNDQ2NDMwMDk4MjI3MiIsImlhdCI6MTYxNDAyOTc0Nn0.6WQiU4D5HHRi3sliHOQe1hsW-hZTEttvdtZuNIdviNI", - }, - }); - - return await res.text(); -} diff --git a/api/src/test/test.ts b/api/src/test/test.ts deleted file mode 100644
index c37c762b..00000000 --- a/api/src/test/test.ts +++ /dev/null
@@ -1,3 +0,0 @@ -import { Snowflake } from "@fosscord/util"; - -console.log(Snowflake.deconstruct("0")); diff --git a/api/src/util/ApiError.ts b/api/src/util/ApiError.ts new file mode 100644
index 00000000..2316cd71 --- /dev/null +++ b/api/src/util/ApiError.ts
@@ -0,0 +1,23 @@ +export class ApiError extends Error { + constructor(readonly message: string, public readonly code: number, public readonly httpStatus: number = 400, public readonly defaultParams?: string[]) { + super(message); + } + + withDefaultParams(): ApiError { + if(this.defaultParams) + return new ApiError(applyParamsToString(this.message, this.defaultParams), this.code, this.httpStatus) + return this + } + + withParams(...params: string[]): ApiError { + return new ApiError(applyParamsToString(this.message, params), this.code, this.httpStatus) + } +} + +export function applyParamsToString(s: string, params: string[]): string { + let newString = s + params.forEach(a => { + newString = newString.replace("{}", a) + }) + return newString +} diff --git a/api/src/util/Channel.ts b/api/src/util/Channel.ts deleted file mode 100644
index fb6f9c8c..00000000 --- a/api/src/util/Channel.ts +++ /dev/null
@@ -1,63 +0,0 @@ -import { - ChannelCreateEvent, - ChannelModel, - ChannelType, - emitEvent, - getPermission, - GuildModel, - Snowflake, - TextChannel, - toObject, - VoiceChannel -} from "@fosscord/util"; -import { HTTPError } from "lambert-server"; - -// TODO: DM channel -export async function createChannel( - channel: Partial<TextChannel | VoiceChannel>, - user_id: string = "0", - opts?: { - keepId?: boolean; - skipExistsCheck?: boolean; - } -) { - // Always check if user has permission first - const permissions = await getPermission(user_id, channel.guild_id); - permissions.hasThrow("MANAGE_CHANNELS"); - - switch (channel.type) { - case ChannelType.GUILD_TEXT: - case ChannelType.GUILD_VOICE: - if (channel.parent_id && !opts?.skipExistsCheck) { - const exists = await ChannelModel.findOne({ id: channel.parent_id }, { guild_id: true }).exec(); - if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400); - if (exists.guild_id !== channel.guild_id) throw new HTTPError("The category channel needs to be in the guild"); - } - break; - case ChannelType.GUILD_CATEGORY: - break; - case ChannelType.DM: - case ChannelType.GROUP_DM: - throw new HTTPError("You can't create a dm channel in a guild"); - // TODO: check if guild is community server - case ChannelType.GUILD_STORE: - case ChannelType.GUILD_NEWS: - default: - throw new HTTPError("Not yet supported"); - } - - if (!channel.permission_overwrites) channel.permission_overwrites = []; - // TODO: auto generate position - - channel = await new ChannelModel({ - ...channel, - ...(!opts?.keepId && { id: Snowflake.generate() }), - created_at: new Date(), - // @ts-ignore - recipient_ids: null - }).save(); - - await emitEvent({ event: "CHANNEL_CREATE", data: toObject(channel), guild_id: channel.guild_id } as ChannelCreateEvent); - - return channel; -} diff --git a/api/src/util/Constants.ts b/api/src/util/Constants.ts
index f3a8dd67..15fdc519 100644 --- a/api/src/util/Constants.ts +++ b/api/src/util/Constants.ts
@@ -1,3 +1,5 @@ +import {ApiError} from "./ApiError"; + export const WSCodes = { 1000: "WS_CLOSE_REQUESTED", 4004: "TOKEN_INVALID", @@ -421,6 +423,7 @@ export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"] /** * An error encountered while performing an API request. Here are the potential errors: + * * GENERAL_ERROR * * UNKNOWN_ACCOUNT * * UNKNOWN_APPLICATION * * UNKNOWN_CHANNEL @@ -436,27 +439,70 @@ export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"] * * UNKNOWN_USER * * UNKNOWN_EMOJI * * UNKNOWN_WEBHOOK + * * UNKNOWN_WEBHOOK_SERVICE + * * UNKNOWN_SESSION * * UNKNOWN_BAN + * * UNKNOWN_SKU + * * UNKNOWN_STORE_LISTING + * * UNKNOWN_ENTITLEMENT + * * UNKNOWN_BUILD + * * UNKNOWN_LOBBY + * * UNKNOWN_BRANCH + * * UNKNOWN_STORE_DIRECTORY_LAYOUT + * * UNKNOWN_REDISTRIBUTABLE + * * UNKNOWN_GIFT_CODE + * * UNKNOWN_STREAM + * * UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN * * UNKNOWN_GUILD_TEMPLATE + * * UNKNOWN_DISCOVERABLE_SERVER_CATEGORY + * * UNKNOWN_STICKER + * * UNKNOWN_INTERACTION + * * UNKNOWN_APPLICATION_COMMAND + * * UNKNOWN_APPLICATION_COMMAND_PERMISSIONS + * * UNKNOWN_STAGE_INSTANCE + * * UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM + * * UNKNOWN_GUILD_WELCOME_SCREEN + * * UNKNOWN_GUILD_SCHEDULED_EVENT + * * UNKNOWN_GUILD_SCHEDULED_EVENT_USER * * BOT_PROHIBITED_ENDPOINT * * BOT_ONLY_ENDPOINT - * * CHANNEL_HIT_WRITE_RATELIMIT + * * EXPLICIT_CONTENT_CANNOT_BE_SENT_TO_RECIPIENT + * * ACTION_NOT_AUTHORIZED_ON_APPLICATION + * * SLOWMODE_RATE_LIMIT + * * ONLY_OWNER + * * ANNOUNCEMENT_RATE_LIMITS + * * CHANNEL_WRITE_RATELIMIT + * * WORDS_NOT_ALLOWED + * * GUILD_PREMIUM_LEVEL_TOO_LOW * * MAXIMUM_GUILDS * * MAXIMUM_FRIENDS * * MAXIMUM_PINS + * * MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED * * MAXIMUM_ROLES * * MAXIMUM_WEBHOOKS + * * MAXIMUM_NUMBER_OF_EMOJIS_REACHED * * MAXIMUM_REACTIONS * * MAXIMUM_CHANNELS * * MAXIMUM_ATTACHMENTS * * MAXIMUM_INVITES + * * MAXIMUM_ANIMATED_EMOJIS + * * MAXIMUM_SERVER_MEMBERS + * * MAXIMUM_SERVER_CATEGORIES * * GUILD_ALREADY_HAS_TEMPLATE + * * MAXIMUM_THREAD_PARTICIPANTS + * * MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS + * * MAXIMUM_BANS_FETCHES + * * MAXIMUM_STICKERS + * * MAXIMUM_PRUNE_REQUESTS * * UNAUTHORIZED * * ACCOUNT_VERIFICATION_REQUIRED + * * OPENING_DIRECT_MESSAGES_TOO_FAST * * REQUEST_ENTITY_TOO_LARGE * * FEATURE_TEMPORARILY_DISABLED * * USER_BANNED + * * TARGET_USER_IS_NOT_CONNECTED_TO_VOICE * * ALREADY_CROSSPOSTED + * * APPLICATION_COMMAND_ALREADY_EXISTS * * MISSING_ACCESS * * INVALID_ACCOUNT_TYPE * * CANNOT_EXECUTE_ON_DM @@ -476,81 +522,196 @@ export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"] * * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL * * INVALID_OR_TAKEN_INVITE_CODE * * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE + * * CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE * * INVALID_OAUTH_TOKEN + * * MISSING_REQUIRED_OAUTH2_SCOPE + * * INVALID_WEBHOOK_TOKEN_PROVIDED + * * INVALID_ROLE + * * INVALID_RECIPIENT * * BULK_DELETE_MESSAGE_TOO_OLD * * INVALID_FORM_BODY * * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT * * INVALID_API_VERSION + * * FILE_EXCEEDS_MAXIMUM_SIZE + * * INVALID_FILE_UPLOADED + * * CANNOT_SELF_REDEEM_GIFT + * * PAYMENT_SOURCE_REQUIRED * * CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL + * * INVALID_STICKER_SENT + * * CANNOT_EDIT_ARCHIVED_THREAD + * * INVALID_THREAD_NOTIFICATION_SETTINGS + * * BEFORE_EARLIER_THAN_THREAD_CREATION_DATE + * * SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION + * * SERVER_NEEDS_MONETIZATION_ENABLED + * * TWO_FACTOR_REQUIRED + * * NO_USERS_WITH_DISCORDTAG_EXIST * * REACTION_BLOCKED * * RESOURCE_OVERLOADED + * * STAGE_ALREADY_OPEN + * * THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE + * * THREAD_IS_LOCKED + * * MAXIMUM_NUMBER_OF_ACTIVE_THREADS + * * MAXIMUM_NUMBER_OF_ACTIVE_ANNOUNCEMENT_THREADS + * * INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE + * * LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES + * * STICKER_MAXIMUM_FRAMERATE + * * STICKER_MAXIMUM_FRAME_COUNT + * * LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS + * * STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE + * * STICKER_ANIMATION_DURATION_MAXIMUM + * * UNKNOWN_VOICE_STATE * @typedef {string} APIError */ -export const APIErrors = { - UNKNOWN_ACCOUNT: 10001, - UNKNOWN_APPLICATION: 10002, - UNKNOWN_CHANNEL: 10003, - UNKNOWN_GUILD: 10004, - UNKNOWN_INTEGRATION: 10005, - UNKNOWN_INVITE: 10006, - UNKNOWN_MEMBER: 10007, - UNKNOWN_MESSAGE: 10008, - UNKNOWN_OVERWRITE: 10009, - UNKNOWN_PROVIDER: 10010, - UNKNOWN_ROLE: 10011, - UNKNOWN_TOKEN: 10012, - UNKNOWN_USER: 10013, - UNKNOWN_EMOJI: 10014, - UNKNOWN_WEBHOOK: 10015, - UNKNOWN_BAN: 10026, - UNKNOWN_GUILD_TEMPLATE: 10057, - BOT_PROHIBITED_ENDPOINT: 20001, - BOT_ONLY_ENDPOINT: 20002, - CHANNEL_HIT_WRITE_RATELIMIT: 20028, - MAXIMUM_GUILDS: 30001, - MAXIMUM_FRIENDS: 30002, - MAXIMUM_PINS: 30003, - MAXIMUM_ROLES: 30005, - MAXIMUM_WEBHOOKS: 30007, - MAXIMUM_REACTIONS: 30010, - MAXIMUM_CHANNELS: 30013, - MAXIMUM_ATTACHMENTS: 30015, - MAXIMUM_INVITES: 30016, - GUILD_ALREADY_HAS_TEMPLATE: 30031, - UNAUTHORIZED: 40001, - ACCOUNT_VERIFICATION_REQUIRED: 40002, - REQUEST_ENTITY_TOO_LARGE: 40005, - FEATURE_TEMPORARILY_DISABLED: 40006, - USER_BANNED: 40007, - ALREADY_CROSSPOSTED: 40033, - MISSING_ACCESS: 50001, - INVALID_ACCOUNT_TYPE: 50002, - CANNOT_EXECUTE_ON_DM: 50003, - EMBED_DISABLED: 50004, - CANNOT_EDIT_MESSAGE_BY_OTHER: 50005, - CANNOT_SEND_EMPTY_MESSAGE: 50006, - CANNOT_MESSAGE_USER: 50007, - CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: 50008, - CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: 50009, - OAUTH2_APPLICATION_BOT_ABSENT: 50010, - MAXIMUM_OAUTH2_APPLICATIONS: 50011, - INVALID_OAUTH_STATE: 50012, - MISSING_PERMISSIONS: 50013, - INVALID_AUTHENTICATION_TOKEN: 50014, - NOTE_TOO_LONG: 50015, - INVALID_BULK_DELETE_QUANTITY: 50016, - CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: 50019, - INVALID_OR_TAKEN_INVITE_CODE: 50020, - CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: 50021, - INVALID_OAUTH_TOKEN: 50025, - BULK_DELETE_MESSAGE_TOO_OLD: 50034, - INVALID_FORM_BODY: 50035, - INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: 50036, - INVALID_API_VERSION: 50041, - CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: 50074, - REACTION_BLOCKED: 90001, - RESOURCE_OVERLOADED: 130000, -}; +export const DiscordApiErrors = { + //https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes + GENERAL_ERROR: new ApiError("General error (such as a malformed request body, amongst other things)", 0), + UNKNOWN_ACCOUNT: new ApiError("Unknown account", 10001), + UNKNOWN_APPLICATION: new ApiError("Unknown application", 10002), + UNKNOWN_CHANNEL: new ApiError("Unknown channel", 10003), + UNKNOWN_GUILD: new ApiError("Unknown guild", 10004), + UNKNOWN_INTEGRATION: new ApiError("Unknown integration", 10005), + UNKNOWN_INVITE: new ApiError("Unknown invite", 10006), + UNKNOWN_MEMBER: new ApiError("Unknown member", 10007), + UNKNOWN_MESSAGE: new ApiError("Unknown message", 10008), + UNKNOWN_OVERWRITE: new ApiError("Unknown permission overwrite", 10009), + UNKNOWN_PROVIDER: new ApiError("Unknown provider", 10010), + UNKNOWN_ROLE: new ApiError("Unknown role", 10011), + UNKNOWN_TOKEN: new ApiError("Unknown token", 10012), + UNKNOWN_USER: new ApiError("Unknown user", 10013), + UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014), + UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015), + UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016), + UNKNOWN_SESSION: new ApiError("Unknown session", 10020), + UNKNOWN_BAN: new ApiError("Unknown ban", 10026), + UNKNOWN_SKU: new ApiError("Unknown SKU", 10027), + UNKNOWN_STORE_LISTING: new ApiError("Unknown Store Listing", 10028), + UNKNOWN_ENTITLEMENT: new ApiError("Unknown entitlement", 10029), + UNKNOWN_BUILD: new ApiError("Unknown build", 10030), + UNKNOWN_LOBBY: new ApiError("Unknown lobby", 10031), + UNKNOWN_BRANCH: new ApiError("Unknown branch", 10032), + UNKNOWN_STORE_DIRECTORY_LAYOUT: new ApiError("Unknown store directory layout", 10033), + UNKNOWN_REDISTRIBUTABLE: new ApiError("Unknown redistributable", 10036), + UNKNOWN_GIFT_CODE: new ApiError("Unknown gift code", 10038), + UNKNOWN_STREAM: new ApiError("Unknown stream", 10049), + UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN: new ApiError("Unknown premium server subscribe cooldown", 10050), + UNKNOWN_GUILD_TEMPLATE: new ApiError("Unknown guild template", 10057), + UNKNOWN_DISCOVERABLE_SERVER_CATEGORY: new ApiError("Unknown discoverable server category", 10059), + UNKNOWN_STICKER: new ApiError("Unknown sticker", 10060), + UNKNOWN_INTERACTION: new ApiError("Unknown interaction", 10062), + UNKNOWN_APPLICATION_COMMAND: new ApiError("Unknown application command", 10063), + UNKNOWN_APPLICATION_COMMAND_PERMISSIONS: new ApiError("Unknown application command permissions", 10066), + UNKNOWN_STAGE_INSTANCE: new ApiError("Unknown Stage Instance", 10067), + UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM: new ApiError("Unknown Guild Member Verification Form", 10068), + UNKNOWN_GUILD_WELCOME_SCREEN: new ApiError("Unknown Guild Welcome Screen", 10069), + UNKNOWN_GUILD_SCHEDULED_EVENT: new ApiError("Unknown Guild Scheduled Event", 10070), + UNKNOWN_GUILD_SCHEDULED_EVENT_USER: new ApiError("Unknown Guild Scheduled Event User", 10071), + BOT_PROHIBITED_ENDPOINT: new ApiError("Bots cannot use this endpoint", 20001), + BOT_ONLY_ENDPOINT: new ApiError("Only bots can use this endpoint", 20002), + EXPLICIT_CONTENT_CANNOT_BE_SENT_TO_RECIPIENT: new ApiError("Explicit content cannot be sent to the desired recipient(s)", 20009), + ACTION_NOT_AUTHORIZED_ON_APPLICATION: new ApiError("You are not authorized to perform this action on this application", 20012), + SLOWMODE_RATE_LIMIT: new ApiError("This action cannot be performed due to slowmode rate limit", 20016), + ONLY_OWNER: new ApiError("Only the owner of this account can perform this action", 20018), + ANNOUNCEMENT_RATE_LIMITS: new ApiError("This message cannot be edited due to announcement rate limits", 20022), + CHANNEL_WRITE_RATELIMIT: new ApiError("The channel you are writing has hit the write rate limit", 20028), + WORDS_NOT_ALLOWED: new ApiError("Your Stage topic, server name, server description, or channel names contain words that are not allowed", 20031), + GUILD_PREMIUM_LEVEL_TOO_LOW: new ApiError("Guild premium subscription level too low", 20035), + MAXIMUM_GUILDS: new ApiError("Maximum number of guilds reached ({})", 30001, undefined, ["100"]), + MAXIMUM_FRIENDS: new ApiError("Maximum number of friends reached ({})", 30002, undefined, ["1000"]), + MAXIMUM_PINS: new ApiError("Maximum number of pins reached for the channel ({})", 30003, undefined, ["50"]), + MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED: new ApiError("Maximum number of recipients reached ({})", 30004, undefined, ["10"]), + MAXIMUM_ROLES: new ApiError("Maximum number of guild roles reached ({})", 30005, undefined, ["250"]), + MAXIMUM_WEBHOOKS: new ApiError("Maximum number of webhooks reached ({})", 30007, undefined, ["10"]), + MAXIMUM_NUMBER_OF_EMOJIS_REACHED: new ApiError("Maximum number of emojis reached", 30008), + MAXIMUM_REACTIONS: new ApiError("Maximum number of reactions reached ({})", 30010, undefined, ["20"]), + MAXIMUM_CHANNELS: new ApiError("Maximum number of guild channels reached ({})", 30013, undefined, ["500"]), + MAXIMUM_ATTACHMENTS: new ApiError("Maximum number of attachments in a message reached ({})", 30015, undefined, ["10"]), + MAXIMUM_INVITES: new ApiError("Maximum number of invites reached ({})", 30016, undefined, ["1000"]), + MAXIMUM_ANIMATED_EMOJIS: new ApiError("Maximum number of animated emojis reached", 30018), + MAXIMUM_SERVER_MEMBERS: new ApiError("Maximum number of server members reached", 30019), + MAXIMUM_SERVER_CATEGORIES: new ApiError("Maximum number of server categories has been reached ({})", 30030, undefined, ["5"]), + GUILD_ALREADY_HAS_TEMPLATE: new ApiError("Guild already has a template", 30031), + MAXIMUM_THREAD_PARTICIPANTS: new ApiError("Max number of thread participants has been reached", 30033), + MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS: new ApiError("Maximum number of bans for non-guild members have been exceeded", 30035), + MAXIMUM_BANS_FETCHES: new ApiError("Maximum number of bans fetches has been reached", 30037), + MAXIMUM_STICKERS: new ApiError("Maximum number of stickers reached", 30039), + MAXIMUM_PRUNE_REQUESTS: new ApiError("Maximum number of prune requests has been reached. Try again later", 30040), + UNAUTHORIZED: new ApiError("Unauthorized. Provide a valid token and try again", 40001), + ACCOUNT_VERIFICATION_REQUIRED: new ApiError("You need to verify your account in order to perform this action", 40002), + OPENING_DIRECT_MESSAGES_TOO_FAST: new ApiError("You are opening direct messages too fast", 40003), + REQUEST_ENTITY_TOO_LARGE: new ApiError("Request entity too large. Try sending something smaller in size", 40005), + FEATURE_TEMPORARILY_DISABLED: new ApiError("This feature has been temporarily disabled server-side", 40006), + USER_BANNED: new ApiError("The user is banned from this guild", 40007), + TARGET_USER_IS_NOT_CONNECTED_TO_VOICE: new ApiError("Target user is not connected to voice", 40032), + ALREADY_CROSSPOSTED: new ApiError("This message has already been crossposted", 40033), + APPLICATION_COMMAND_ALREADY_EXISTS: new ApiError("An application command with that name already exists", 40041), + MISSING_ACCESS: new ApiError("Missing access", 50001), + INVALID_ACCOUNT_TYPE: new ApiError("Invalid account type", 50002), + CANNOT_EXECUTE_ON_DM: new ApiError("Cannot execute action on a DM channel", 50003), + EMBED_DISABLED: new ApiError("Guild widget disabled", 50004), + CANNOT_EDIT_MESSAGE_BY_OTHER: new ApiError("Cannot edit a message authored by another user", 50005), + CANNOT_SEND_EMPTY_MESSAGE: new ApiError("Cannot send an empty message", 50006), + CANNOT_MESSAGE_USER: new ApiError("Cannot send messages to this user", 50007), + CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: new ApiError("Cannot send messages in a voice channel", 50008), + CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: new ApiError("Channel verification level is too high for you to gain access", 50009), + OAUTH2_APPLICATION_BOT_ABSENT: new ApiError("OAuth2 application does not have a bot", 50010), + MAXIMUM_OAUTH2_APPLICATIONS: new ApiError("OAuth2 application limit reached", 50011), + INVALID_OAUTH_STATE: new ApiError("Invalid OAuth2 state", 50012), + MISSING_PERMISSIONS: new ApiError("You lack permissions to perform that action", 50013), + INVALID_AUTHENTICATION_TOKEN: new ApiError("Invalid authentication token provided", 50014), + NOTE_TOO_LONG: new ApiError("Note was too long", 50015), + INVALID_BULK_DELETE_QUANTITY: new ApiError("Provided too few or too many messages to delete. Must provide at least {} and fewer than {} messages to delete", 50016, undefined, ["2","100"]), + CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: new ApiError("A message can only be pinned to the channel it was sent in", 50019), + INVALID_OR_TAKEN_INVITE_CODE: new ApiError("Invite code was either invalid or taken", 50020), + CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: new ApiError("Cannot execute action on a system message", 50021), + CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE: new ApiError("Cannot execute action on this channel type", 50024), + INVALID_OAUTH_TOKEN: new ApiError("Invalid OAuth2 access token provided", 50025), + MISSING_REQUIRED_OAUTH2_SCOPE: new ApiError("Missing required OAuth2 scope", 50026), + INVALID_WEBHOOK_TOKEN_PROVIDED: new ApiError("Invalid webhook token provided", 50027), + INVALID_ROLE: new ApiError("Invalid role", 50028), + INVALID_RECIPIENT: new ApiError("Invalid Recipient(s)", 50033), + BULK_DELETE_MESSAGE_TOO_OLD: new ApiError("A message provided was too old to bulk delete", 50034), + INVALID_FORM_BODY: new ApiError("Invalid form body (returned for both application/json and multipart/form-data bodies), or invalid Content-Type provided", 50035), + INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: new ApiError("An invite was accepted to a guild the application's bot is not in", 50036), + INVALID_API_VERSION: new ApiError("Invalid API version provided", 50041), + FILE_EXCEEDS_MAXIMUM_SIZE: new ApiError("File uploaded exceeds the maximum size", 50045), + INVALID_FILE_UPLOADED: new ApiError("Invalid file uploaded", 50046), + CANNOT_SELF_REDEEM_GIFT: new ApiError("Cannot self-redeem this gift", 50054), + PAYMENT_SOURCE_REQUIRED: new ApiError("Payment source required to redeem gift", 50070), + CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: new ApiError("Cannot delete a channel required for Community guilds", 50074), + INVALID_STICKER_SENT: new ApiError("Invalid sticker sent", 50081), + CANNOT_EDIT_ARCHIVED_THREAD: new ApiError("Tried to perform an operation on an archived thread, such as editing a message or adding a user to the thread", 50083), + INVALID_THREAD_NOTIFICATION_SETTINGS: new ApiError("Invalid thread notification settings", 50084), + BEFORE_EARLIER_THAN_THREAD_CREATION_DATE: new ApiError("before value is earlier than the thread creation date", 50085), + SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION: new ApiError("This server is not available in your location", 50095), + SERVER_NEEDS_MONETIZATION_ENABLED: new ApiError("This server needs monetization enabled in order to perform this action", 50097), + TWO_FACTOR_REQUIRED: new ApiError("Two factor is required for this operation", 60003), + NO_USERS_WITH_DISCORDTAG_EXIST: new ApiError("No users with DiscordTag exist", 80004), + REACTION_BLOCKED: new ApiError("Reaction was blocked", 90001), + RESOURCE_OVERLOADED: new ApiError("API resource is currently overloaded. Try again a little later", 130000), + STAGE_ALREADY_OPEN: new ApiError("The Stage is already open", 150006), + THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE: new ApiError("A thread has already been created for this message", 160004), + THREAD_IS_LOCKED: new ApiError("Thread is locked", 160005), + MAXIMUM_NUMBER_OF_ACTIVE_THREADS: new ApiError("Maximum number of active threads reached", 160006), + MAXIMUM_NUMBER_OF_ACTIVE_ANNOUNCEMENT_THREADS: new ApiError("Maximum number of active announcement threads reached", 160007), + INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: new ApiError("Invalid JSON for uploaded Lottie file", 170001), + LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES: new ApiError("Uploaded Lotties cannot contain rasterized images such as PNG or JPEG", 170002), + STICKER_MAXIMUM_FRAMERATE: new ApiError("Sticker maximum framerate exceeded", 170003), + STICKER_MAXIMUM_FRAME_COUNT: new ApiError("Sticker frame count exceeds maximum of {} frames", 170004, undefined, ["1000"]), + LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS: new ApiError("Lottie animation maximum dimensions exceeded", 170005), + STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE: new ApiError("Sticker frame rate is either too small or too large", 170006), + STICKER_ANIMATION_DURATION_MAXIMUM: new ApiError("Sticker animation duration exceeds maximum of {} seconds", 170007, undefined, ["5"]), + + + //Other errors + UNKNOWN_VOICE_STATE: new ApiError("Unknown Voice State", 10065, 404), +} + +/** + * An error encountered while performing an API request (Fosscord only). Here are the potential errors: + */ +export const FosscordApiErrors = { + +} /** * The value set for a guild's default message notifications, e.g. `ALL`. Here are the available types: diff --git a/api/src/util/Member.ts b/api/src/util/Member.ts deleted file mode 100644
index da02735c..00000000 --- a/api/src/util/Member.ts +++ /dev/null
@@ -1,222 +0,0 @@ -import { - Guild, - GuildCreateEvent, - GuildDeleteEvent, - GuildMemberAddEvent, - GuildMemberRemoveEvent, - GuildMemberUpdateEvent, - GuildModel, - MemberModel, - RoleModel, - toObject, - UserModel, - GuildDocument, - Config, - emitEvent -} from "@fosscord/util"; - -import { HTTPError } from "lambert-server"; - -import { getPublicUser } from "./User"; - -export const PublicMemberProjection = { - id: true, - guild_id: true, - nick: true, - roles: true, - joined_at: true, - pending: true, - deaf: true, - mute: true, - premium_since: true -}; - -export async function isMember(user_id: string, guild_id: string) { - const exists = await MemberModel.exists({ id: user_id, guild_id }); - if (!exists) throw new HTTPError("You are not a member of this guild", 403); - return exists; -} - -export async function addMember(user_id: string, guild_id: string, cache?: { guild?: GuildDocument }) { - const user = await getPublicUser(user_id, { guilds: true }); - - const { maxGuilds } = Config.get().limits.user; - if (user.guilds.length >= maxGuilds) { - throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403); - } - - const guild = cache?.guild || (await GuildModel.findOne({ id: guild_id }).exec()); - - if (!guild) throw new HTTPError("Guild not found", 404); - - if (await MemberModel.exists({ id: user.id, guild_id })) throw new HTTPError("You are already a member of this guild", 400); - - const member = { - id: user_id, - guild_id: guild_id, - nick: undefined, - roles: [guild_id], // @everyone role - joined_at: new Date(), - premium_since: undefined, - deaf: false, - mute: false, - pending: false - }; - - await Promise.all([ - new MemberModel({ - ...member, - read_state: {}, - settings: { - channel_overrides: [], - message_notifications: 0, - mobile_push: true, - mute_config: null, - muted: false, - suppress_everyone: false, - suppress_roles: false, - version: 0 - } - }).save(), - - UserModel.updateOne({ id: user_id }, { $push: { guilds: guild_id } }).exec(), - GuildModel.updateOne({ id: guild_id }, { $inc: { member_count: 1 } }).exec(), - - emitEvent({ - event: "GUILD_MEMBER_ADD", - data: { - ...member, - user, - guild_id: guild_id - }, - guild_id: guild_id - } as GuildMemberAddEvent) - ]); - - await emitEvent({ - event: "GUILD_CREATE", - data: toObject( - await guild - .populate({ path: "members", match: { guild_id } }) - .populate({ path: "joined_at", match: { id: user.id } }) - .execPopulate() - ), - user_id - } as GuildCreateEvent); -} - -export async function removeMember(user_id: string, guild_id: string) { - const user = await getPublicUser(user_id); - - const guild = await GuildModel.findOne({ id: guild_id }, { owner_id: true }).exec(); - if (!guild) throw new HTTPError("Guild not found", 404); - if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild"); - if (!(await MemberModel.exists({ id: user.id, guild_id }))) throw new HTTPError("Is not member of this guild", 404); - - // use promise all to execute all promises at the same time -> save time - return Promise.all([ - MemberModel.deleteOne({ - id: user_id, - guild_id: guild_id - }).exec(), - UserModel.updateOne({ id: user.id }, { $pull: { guilds: guild_id } }).exec(), - GuildModel.updateOne({ id: guild_id }, { $inc: { member_count: -1 } }).exec(), - - emitEvent({ - event: "GUILD_DELETE", - data: { - id: guild_id - }, - user_id: user_id - } as GuildDeleteEvent), - emitEvent({ - event: "GUILD_MEMBER_REMOVE", - data: { - guild_id: guild_id, - user: user - }, - guild_id: guild_id - } as GuildMemberRemoveEvent) - ]); -} - -export async function addRole(user_id: string, guild_id: string, role_id: string) { - const user = await getPublicUser(user_id); - - const role = await RoleModel.findOne({ id: role_id, guild_id: guild_id }).exec(); - if (!role) throw new HTTPError("role not found", 404); - - var memberObj = await MemberModel.findOneAndUpdate( - { - id: user_id, - guild_id: guild_id - }, - { $push: { roles: role_id } }, - { new: true } - ).exec(); - - if (!memberObj) throw new HTTPError("Member not found", 404); - - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { - guild_id: guild_id, - user: user, - roles: memberObj.roles - }, - guild_id: guild_id - } as GuildMemberUpdateEvent); -} - -export async function removeRole(user_id: string, guild_id: string, role_id: string) { - const user = await getPublicUser(user_id); - - const role = await RoleModel.findOne({ id: role_id, guild_id: guild_id }).exec(); - if (!role) throw new HTTPError("role not found", 404); - - var memberObj = await MemberModel.findOneAndUpdate( - { - id: user_id, - guild_id: guild_id - }, - { $pull: { roles: role_id } }, - { new: true } - ).exec(); - - if (!memberObj) throw new HTTPError("Member not found", 404); - - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { - guild_id: guild_id, - user: user, - roles: memberObj.roles - }, - guild_id: guild_id - } as GuildMemberUpdateEvent); -} - -export async function changeNickname(user_id: string, guild_id: string, nickname: string) { - const user = await getPublicUser(user_id); - - var memberObj = await MemberModel.findOneAndUpdate( - { - id: user_id, - guild_id: guild_id - }, - { nick: nickname }, - { new: true } - ).exec(); - - if (!memberObj) throw new HTTPError("Member not found", 404); - - await emitEvent({ - event: "GUILD_MEMBER_UPDATE", - data: { - guild_id: guild_id, - user: user, - nick: nickname - }, - guild_id: guild_id - } as GuildMemberUpdateEvent); -} diff --git a/api/src/util/Message.ts b/api/src/util/Message.ts
index 8a1e959e..fea553bc 100644 --- a/api/src/util/Message.ts +++ b/api/src/util/Message.ts
@@ -1,5 +1,5 @@ import { - ChannelModel, + Channel, Embed, emitEvent, Message, @@ -7,21 +7,23 @@ import { MessageUpdateEvent, getPermission, CHANNEL_MENTION, - toObject, - MessageModel, Snowflake, - PublicMemberProjection, USER_MENTION, ROLE_MENTION, - RoleModel, + Role, EVERYONE_MENTION, - HERE_MENTION + HERE_MENTION, + MessageType, + User, + Application, + Webhook, + Attachment } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import fetch from "node-fetch"; import cheerio from "cheerio"; +import { MessageCreateSchema } from "../schema/Message"; -import { MessageType } from "@fosscord/util/dist/util/Constants"; // TODO: check webhook, application, system author const LINK_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; @@ -37,19 +39,37 @@ const DEFAULT_FETCH_OPTIONS: any = { method: "GET" }; -export async function handleMessage(opts: Partial<Message>) { - 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(); +export async function handleMessage(opts: MessageOptions): Promise<Message> { + const channel = await Channel.findOneOrFail({ where: { id: opts.channel_id }, relations: ["recipients"] }); if (!channel || !opts.channel_id) throw new HTTPError("Channel not found", 404); + + const message = new Message({ + ...opts, + guild_id: channel.guild_id, + channel_id: opts.channel_id, + attachments: opts.attachments || [], + embeds: opts.embeds || [], + reactions: /*opts.reactions ||*/ [], + type: opts.type ?? 0 + }); + // TODO: are tts messages allowed in dm channels? should permission be checked? + if (opts.author_id) { + message.author = await User.getPublicUser(opts.author_id); + } + if (opts.application_id) { + message.application = await Application.findOneOrFail({ id: opts.application_id }); + } + if (opts.webhook_id) { + message.webhook = await Webhook.findOneOrFail({ id: opts.webhook_id }); + } - // @ts-ignore - const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id, { channel }); + const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id); permission.hasThrow("SEND_MESSAGES"); + if (permission.cache.member) { + message.member = permission.cache.member; + } + if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES"); if (opts.message_reference) { permission.hasThrow("READ_MESSAGE_HISTORY"); @@ -57,24 +77,24 @@ export async function handleMessage(opts: Partial<Message>) { if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel"); // TODO: should be checked if the referenced message exists? // @ts-ignore - opts.type = MessageType.REPLY; + message.type = MessageType.REPLY; } - if (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.stickers?.length && !opts.activity) { + // TODO: stickers/activity + if (!opts.content && !opts.embeds?.length && !opts.attachments?.length) { throw new HTTPError("Empty messages are not allowed", 50006); } var content = opts.content; - var mention_channels_ids = [] as string[]; + var mention_channel_ids = [] as string[]; var mention_role_ids = [] as string[]; var mention_user_ids = [] as string[]; var mention_everyone = false; - var mention_everyone = false; if (content) { - content = content.trim(); + message.content = content.trim(); for (const [_, mention] of content.matchAll(CHANNEL_MENTION)) { - if (!mention_channels_ids.includes(mention)) mention_channels_ids.push(mention); + if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(mention); } for (const [_, mention] of content.matchAll(USER_MENTION)) { @@ -83,7 +103,7 @@ export async function handleMessage(opts: Partial<Message>) { await Promise.all( Array.from(content.matchAll(ROLE_MENTION)).map(async ([_, mention]) => { - const role = await RoleModel.findOne({ id: mention, guild_id: channel.guild_id }).exec(); + const role = await Role.findOneOrFail({ id: mention, guild_id: channel.guild_id }); if (role.mentionable || permission.has("MANAGE_ROLES")) { mention_role_ids.push(mention); } @@ -95,20 +115,14 @@ export async function handleMessage(opts: Partial<Message>) { } } + message.mention_channels = mention_channel_ids.map((x) => new Channel({ id: x })); + message.mention_roles = mention_role_ids.map((x) => new Role({ id: x })); + message.mentions = mention_user_ids.map((x) => new User({ id: x })); + message.mention_everyone = mention_everyone; + // TODO: check and put it all in the body - return { - ...opts, - guild_id: channel.guild_id, - channel_id: opts.channel_id, - mention_channels_ids, - mention_role_ids, - mention_user_ids, - mention_everyone, - attachments: opts.attachments || [], - embeds: opts.embeds || [], - reactions: opts.reactions || [], - type: opts.type ?? 0 - }; + + return message; } // TODO: cache link result in db @@ -160,20 +174,33 @@ export async function postHandleMessage(message: Message) { channel_id: message.channel_id, data } as MessageUpdateEvent), - MessageModel.updateOne({ id: message.id, channel_id: message.channel_id }, data).exec() + Message.update({ id: message.id, channel_id: message.channel_id }, data) ]); } -export async function sendMessage(opts: Partial<Message>) { - const message = await handleMessage({ ...opts, id: Snowflake.generate(), timestamp: new Date() }); +export async function sendMessage(opts: MessageOptions) { + const message = await handleMessage({ ...opts, timestamp: new Date() }); - const data = toObject( - await new MessageModel(message).populate({ path: "member", select: PublicMemberProjection }).populate("referenced_message").save() - ); + await Promise.all([ + message.save(), + emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data: message.toJSON() } as MessageCreateEvent) + ]); - await emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data } as MessageCreateEvent); + postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error - postHandleMessage(data).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error + return message; +} - return data; +interface MessageOptions extends MessageCreateSchema { + id?: string; + type?: MessageType; + pinned?: boolean; + author_id?: string; + webhook_id?: string; + application_id?: string; + embeds?: Embed[]; + channel_id?: string; + attachments?: Attachment[]; + edited_timestamp?: Date; + timestamp?: Date; } diff --git a/api/src/util/User.ts b/api/src/util/User.ts deleted file mode 100644
index 392c7101..00000000 --- a/api/src/util/User.ts +++ /dev/null
@@ -1,16 +0,0 @@ -import { toObject, UserModel, PublicUserProjection } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; - -export { PublicUserProjection }; - -export async function getPublicUser(user_id: string, additional_fields?: any) { - const user = await UserModel.findOne( - { id: user_id }, - { - ...PublicUserProjection, - ...additional_fields - } - ).exec(); - if (!user) throw new HTTPError("User not found", 404); - return toObject(user); -} diff --git a/api/src/util/Voice.ts b/api/src/util/Voice.ts new file mode 100644
index 00000000..087bdfa8 --- /dev/null +++ b/api/src/util/Voice.ts
@@ -0,0 +1,32 @@ +import {Config} from "@fosscord/util"; +import {distanceBetweenLocations, IPAnalysis} from "./ipAddress"; + +export async function getVoiceRegions(ipAddress: string, vip: boolean) { + const regions = Config.get().regions; + const availableRegions = regions.available.filter(ar => vip ? true : !ar.vip); + let optimalId = regions.default + + if(!regions.useDefaultAsOptimal) { + const clientIpAnalysis = await IPAnalysis(ipAddress) + + let min = Number.POSITIVE_INFINITY + + for (let ar of availableRegions) { + //TODO the endpoint location should be saved in the database if not already present to prevent IPAnalysis call + const dist = distanceBetweenLocations(clientIpAnalysis, ar.location || (await IPAnalysis(ar.endpoint))) + + if(dist < min) { + min = dist + optimalId = ar.id + } + } + } + + return availableRegions.map(ar => ({ + id: ar.id, + name: ar.name, + custom: ar.custom, + deprecated: ar.deprecated, + optimal: ar.id === optimalId + })) +} \ No newline at end of file diff --git a/api/src/util/ipAddress.ts b/api/src/util/ipAddress.ts
index 0a724daa..c6239426 100644 --- a/api/src/util/ipAddress.ts +++ b/api/src/util/ipAddress.ts
@@ -60,6 +60,7 @@ const exampleData = { status: 200 }; +//TODO add function that support both ip and domain names export async function IPAnalysis(ip: string): Promise<typeof exampleData> { const { ipdataApiKey } = Config.get().security; if (!ipdataApiKey) return { ...exampleData, ip }; @@ -79,3 +80,19 @@ export function getIpAdress(req: Request): string { // @ts-ignore return req.headers[Config.get().security.forwadedFor] || req.socket.remoteAddress; } + + +export function distanceBetweenLocations(loc1: any, loc2: any): number { + return distanceBetweenCoords(loc1.latitude, loc1.longitude, loc2.latitude, loc2.longitude); +} + +//Haversine function +function distanceBetweenCoords(lat1: number, lon1: number, lat2: number, lon2: number) { + const p = 0.017453292519943295; // Math.PI / 180 + const c = Math.cos; + const a = 0.5 - c((lat2 - lat1) * p) / 2 + + c(lat1 * p) * c(lat2 * p) * + (1 - c((lon2 - lon1) * p)) / 2; + + return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km +} \ No newline at end of file