summary refs log tree commit diff
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/api/Server.ts (renamed from api/src/Server.ts)31
-rw-r--r--src/api/global.d.ts (renamed from api/src/global.d.ts)0
-rw-r--r--src/api/index.ts (renamed from api/src/index.ts)2
-rw-r--r--src/api/middlewares/Authentication.ts (renamed from api/src/middlewares/Authentication.ts)6
-rw-r--r--src/api/middlewares/BodyParser.ts (renamed from api/src/middlewares/BodyParser.ts)2
-rw-r--r--src/api/middlewares/CORS.ts (renamed from api/src/middlewares/CORS.ts)0
-rw-r--r--src/api/middlewares/ErrorHandler.ts (renamed from api/src/middlewares/ErrorHandler.ts)3
-rw-r--r--src/api/middlewares/RateLimit.ts (renamed from api/src/middlewares/RateLimit.ts)19
-rw-r--r--src/api/middlewares/TestClient.ts156
-rw-r--r--src/api/middlewares/Translation.ts (renamed from api/src/middlewares/Translation.ts)10
-rw-r--r--src/api/middlewares/index.ts (renamed from api/src/middlewares/index.ts)0
-rw-r--r--src/api/routes/-/healthz.ts (renamed from api/src/routes/-/healthz.ts)2
-rw-r--r--src/api/routes/-/readyz.ts (renamed from api/src/routes/-/readyz.ts)2
-rw-r--r--src/api/routes/applications/#id/bot/index.ts83
-rw-r--r--src/api/routes/applications/#id/entitlements.ts (renamed from api/src/routes/applications/#id/entitlements.ts)2
-rw-r--r--src/api/routes/applications/#id/index.ts29
-rw-r--r--src/api/routes/applications/#id/skus.ts (renamed from api/src/routes/applications/index.ts)5
-rw-r--r--src/api/routes/applications/detectable.ts (renamed from api/src/routes/applications/detectable.ts)2
-rw-r--r--src/api/routes/applications/index.ts34
-rw-r--r--src/api/routes/auth/location-metadata.ts12
-rw-r--r--src/api/routes/auth/login.ts (renamed from api/src/routes/auth/login.ts)53
-rw-r--r--src/api/routes/auth/mfa/totp.ts36
-rw-r--r--src/api/routes/auth/register.ts (renamed from api/src/routes/auth/register.ts)65
-rw-r--r--src/api/routes/channels/#channel_id/followers.ts (renamed from api/src/routes/channels/#channel_id/followers.ts)2
-rw-r--r--src/api/routes/channels/#channel_id/index.ts (renamed from api/src/routes/channels/#channel_id/index.ts)44
-rw-r--r--src/api/routes/channels/#channel_id/invites.ts58
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/ack.ts (renamed from api/src/routes/channels/#channel_id/messages/#message_id/ack.ts)17
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts (renamed from api/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts)2
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/index.ts (renamed from api/src/routes/channels/#channel_id/messages/#message_id/index.ts)120
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts (renamed from api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts)94
-rw-r--r--src/api/routes/channels/#channel_id/messages/bulk-delete.ts (renamed from api/src/routes/channels/#channel_id/messages/bulk-delete.ts)17
-rw-r--r--src/api/routes/channels/#channel_id/messages/index.ts (renamed from api/src/routes/channels/#channel_id/messages/index.ts)103
-rw-r--r--src/api/routes/channels/#channel_id/permissions.ts (renamed from api/src/routes/channels/#channel_id/permissions.ts)23
-rw-r--r--src/api/routes/channels/#channel_id/pins.ts (renamed from api/src/routes/channels/#channel_id/pins.ts)24
-rw-r--r--src/api/routes/channels/#channel_id/purge.ts77
-rw-r--r--src/api/routes/channels/#channel_id/recipients.ts (renamed from api/src/routes/channels/#channel_id/recipients.ts)7
-rw-r--r--src/api/routes/channels/#channel_id/typing.ts (renamed from api/src/routes/channels/#channel_id/typing.ts)6
-rw-r--r--src/api/routes/channels/#channel_id/webhooks.ts (renamed from api/src/routes/channels/#channel_id/webhooks.ts)21
-rw-r--r--src/api/routes/discoverable-guilds.ts39
-rw-r--r--src/api/routes/discovery.ts (renamed from api/src/routes/discovery.ts)6
-rw-r--r--src/api/routes/downloads.ts20
-rw-r--r--src/api/routes/experiments.ts (renamed from api/src/routes/experiments.ts)6
-rw-r--r--src/api/routes/gateway/bot.ts (renamed from api/src/routes/gateway/bot.ts)4
-rw-r--r--src/api/routes/gateway/index.ts (renamed from api/src/routes/gateway/index.ts)4
-rw-r--r--src/api/routes/gifs/search.ts (renamed from api/src/routes/gifs/search.ts)10
-rw-r--r--src/api/routes/gifs/trending-gifs.ts (renamed from api/src/routes/gifs/trending-gifs.ts)10
-rw-r--r--src/api/routes/gifs/trending.ts (renamed from api/src/routes/gifs/trending.ts)15
-rw-r--r--src/api/routes/guild-recommendations.ts (renamed from api/src/routes/guild-recommendations.ts)15
-rw-r--r--src/api/routes/guilds/#guild_id/audit-logs.ts (renamed from api/src/routes/guilds/#guild_id/audit-logs.ts)5
-rw-r--r--src/api/routes/guilds/#guild_id/bans.ts (renamed from api/src/routes/guilds/#guild_id/bans.ts)75
-rw-r--r--src/api/routes/guilds/#guild_id/channels.ts (renamed from api/src/routes/guilds/#guild_id/channels.ts)12
-rw-r--r--src/api/routes/guilds/#guild_id/delete.ts (renamed from api/src/routes/guilds/#guild_id/delete.ts)7
-rw-r--r--src/api/routes/guilds/#guild_id/discovery-requirements.ts37
-rw-r--r--src/api/routes/guilds/#guild_id/emojis.ts (renamed from api/src/routes/guilds/#guild_id/emojis.ts)43
-rw-r--r--src/api/routes/guilds/#guild_id/index.ts (renamed from api/src/routes/guilds/#guild_id/index.ts)56
-rw-r--r--src/api/routes/guilds/#guild_id/integrations.ts9
-rw-r--r--src/api/routes/guilds/#guild_id/invites.ts (renamed from api/src/routes/guilds/#guild_id/invites.ts)2
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/index.ts (renamed from api/src/routes/guilds/#guild_id/members/#member_id/index.ts)38
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/nick.ts (renamed from api/src/routes/guilds/#guild_id/members/#member_id/nick.ts)10
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts (renamed from api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts)2
-rw-r--r--src/api/routes/guilds/#guild_id/members/index.ts (renamed from api/src/routes/guilds/#guild_id/members/index.ts)5
-rw-r--r--src/api/routes/guilds/#guild_id/premium.ts (renamed from api/src/routes/guilds/#guild_id/premium.ts)2
-rw-r--r--src/api/routes/guilds/#guild_id/prune.ts (renamed from api/src/routes/guilds/#guild_id/prune.ts)25
-rw-r--r--src/api/routes/guilds/#guild_id/regions.ts (renamed from api/src/routes/guilds/#guild_id/regions.ts)7
-rw-r--r--src/api/routes/guilds/#guild_id/roles/#role_id/index.ts (renamed from api/src/routes/guilds/#guild_id/roles/#role_id/index.ts)20
-rw-r--r--src/api/routes/guilds/#guild_id/roles/index.ts (renamed from api/src/routes/guilds/#guild_id/roles/index.ts)42
-rw-r--r--src/api/routes/guilds/#guild_id/stickers.ts (renamed from api/src/routes/guilds/#guild_id/stickers.ts)35
-rw-r--r--src/api/routes/guilds/#guild_id/templates.ts (renamed from api/src/routes/guilds/#guild_id/templates.ts)31
-rw-r--r--src/api/routes/guilds/#guild_id/vanity-url.ts (renamed from api/src/routes/guilds/#guild_id/vanity-url.ts)25
-rw-r--r--src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts (renamed from api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts)40
-rw-r--r--src/api/routes/guilds/#guild_id/webhooks.ts9
-rw-r--r--src/api/routes/guilds/#guild_id/welcome_screen.ts (renamed from api/src/routes/guilds/#guild_id/welcome_screen.ts)20
-rw-r--r--src/api/routes/guilds/#guild_id/widget.json.ts (renamed from api/src/routes/guilds/#guild_id/widget.json.ts)13
-rw-r--r--src/api/routes/guilds/#guild_id/widget.png.ts (renamed from api/src/routes/guilds/#guild_id/widget.png.ts)25
-rw-r--r--src/api/routes/guilds/#guild_id/widget.ts (renamed from api/src/routes/guilds/#guild_id/widget.ts)11
-rw-r--r--src/api/routes/guilds/index.ts (renamed from api/src/routes/guilds/index.ts)22
-rw-r--r--src/api/routes/guilds/templates/index.ts (renamed from api/src/routes/guilds/templates/index.ts)51
-rw-r--r--src/api/routes/invites/index.ts (renamed from api/src/routes/invites/index.ts)24
-rw-r--r--src/api/routes/oauth2/tokens.ts (renamed from api/src/routes/oauth2/tokens.ts)2
-rw-r--r--src/api/routes/outbound-promotions.ts (renamed from api/src/routes/outbound-promotions.ts)2
-rw-r--r--src/api/routes/partners/#guild_id/requirements.ts37
-rw-r--r--src/api/routes/ping.ts (renamed from api/src/routes/ping.ts)6
-rw-r--r--src/api/routes/policies/instance/domains.ts17
-rw-r--r--src/api/routes/policies/instance/index.ts (renamed from api/src/routes/policies/instance/index.ts)5
-rw-r--r--src/api/routes/policies/instance/limits.ts (renamed from api/src/routes/policies/instance/limits.ts)4
-rw-r--r--src/api/routes/scheduled-maintenances/upcoming_json.ts12
-rw-r--r--src/api/routes/science.ts (renamed from api/src/routes/science.ts)2
-rw-r--r--src/api/routes/stage-instances.ts (renamed from api/src/routes/stage-instances.ts)2
-rw-r--r--src/api/routes/sticker-packs/index.ts (renamed from api/src/routes/sticker-packs/index.ts)2
-rw-r--r--src/api/routes/stickers/#sticker_id/index.ts (renamed from api/src/routes/stickers/#sticker_id/index.ts)6
-rw-r--r--src/api/routes/stop.ts (renamed from api/src/routes/stop.ts)13
-rw-r--r--src/api/routes/store/published-listings/applications.ts (renamed from api/src/routes/store/published-listings/applications.ts)2
-rw-r--r--src/api/routes/store/published-listings/applications/#id/subscription-plans.ts (renamed from api/src/routes/store/published-listings/applications/#id/subscription-plans.ts)2
-rw-r--r--src/api/routes/store/published-listings/skus.ts (renamed from api/src/routes/store/published-listings/skus.ts)2
-rw-r--r--src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts313
-rw-r--r--src/api/routes/teams.ts (renamed from api/src/routes/teams.ts)2
-rw-r--r--src/api/routes/template.ts.disabled (renamed from api/src/routes/template.ts.disabled)0
-rw-r--r--src/api/routes/track.ts (renamed from api/src/routes/track.ts)2
-rw-r--r--src/api/routes/updates.ts20
-rw-r--r--src/api/routes/users/#id/index.ts (renamed from api/src/routes/users/#id/index.ts)4
-rw-r--r--src/api/routes/users/#id/profile.ts94
-rw-r--r--src/api/routes/users/#id/relationships.ts46
-rw-r--r--src/api/routes/users/@me/activities/statistics/applications.ts (renamed from api/src/routes/users/@me/activities/statistics/applications.ts)2
-rw-r--r--src/api/routes/users/@me/affinities/guilds.ts (renamed from api/src/routes/users/@me/affinities/guilds.ts)2
-rw-r--r--src/api/routes/users/@me/affinities/users.ts (renamed from api/src/routes/users/@me/affinities/users.ts)2
-rw-r--r--src/api/routes/users/@me/applications/#app_id/entitlements.ts (renamed from api/src/routes/users/@me/applications/#app_id/entitlements.ts)2
-rw-r--r--src/api/routes/users/@me/billing/country-code.ts (renamed from api/src/routes/users/@me/billing/country-code.ts)2
-rw-r--r--src/api/routes/users/@me/billing/payment-sources.ts (renamed from api/src/routes/users/@me/billing/payment-sources.ts)2
-rw-r--r--src/api/routes/users/@me/billing/subscriptions.ts (renamed from api/src/routes/users/@me/billing/subscriptions.ts)2
-rw-r--r--src/api/routes/users/@me/channels.ts (renamed from api/src/routes/users/@me/channels.ts)9
-rw-r--r--src/api/routes/users/@me/connections.ts (renamed from api/src/routes/users/@me/connections.ts)2
-rw-r--r--src/api/routes/users/@me/delete.ts (renamed from api/src/routes/users/@me/delete.ts)14
-rw-r--r--src/api/routes/users/@me/devices.ts (renamed from api/src/routes/users/@me/devices.ts)2
-rw-r--r--src/api/routes/users/@me/disable.ts (renamed from api/src/routes/users/@me/disable.ts)13
-rw-r--r--src/api/routes/users/@me/email-settings.ts (renamed from api/src/routes/users/@me/email-settings.ts)2
-rw-r--r--src/api/routes/users/@me/entitlements.ts (renamed from api/src/routes/users/@me/entitlements.ts)2
-rw-r--r--src/api/routes/users/@me/guilds.ts (renamed from api/src/routes/users/@me/guilds.ts)5
-rw-r--r--src/api/routes/users/@me/guilds/premium/subscription-slots.ts (renamed from api/src/routes/users/@me/guilds/premium/subscription-slots.ts)2
-rw-r--r--src/api/routes/users/@me/index.ts (renamed from api/src/routes/users/@me/index.ts)79
-rw-r--r--src/api/routes/users/@me/library.ts (renamed from api/src/routes/users/@me/library.ts)2
-rw-r--r--src/api/routes/users/@me/mfa/codes.ts48
-rw-r--r--src/api/routes/users/@me/mfa/totp/disable.ts40
-rw-r--r--src/api/routes/users/@me/mfa/totp/enable.ts49
-rw-r--r--src/api/routes/users/@me/notes.ts53
-rw-r--r--src/api/routes/users/@me/relationships.ts (renamed from api/src/routes/users/@me/relationships.ts)68
-rw-r--r--src/api/routes/users/@me/settings.ts (renamed from api/src/routes/users/@me/settings.ts)10
-rw-r--r--src/api/routes/voice/regions.ts (renamed from api/src/routes/voice/regions.ts)5
-rw-r--r--src/api/start.ts (renamed from api/src/start.ts)13
-rw-r--r--src/api/util/entities/AssetCacheItem.ts3
-rw-r--r--src/api/util/entities/blockedEmailDomains.txt (renamed from api/src/util/entities/blockedEmailDomains.txt)0
-rw-r--r--src/api/util/entities/trustedEmailDomains.txt (renamed from api/src/util/entities/trustedEmailDomains.txt)0
-rw-r--r--src/api/util/handlers/Instance.ts (renamed from api/src/util/handlers/Instance.ts)2
-rw-r--r--src/api/util/handlers/Message.ts (renamed from api/src/util/handlers/Message.ts)83
-rw-r--r--src/api/util/handlers/Voice.ts (renamed from api/src/util/handlers/Voice.ts)0
-rw-r--r--src/api/util/handlers/route.ts (renamed from api/src/util/handlers/route.ts)17
-rw-r--r--src/api/util/index.ts (renamed from api/src/util/index.ts)8
-rw-r--r--src/api/util/utility/Base64.ts (renamed from api/src/util/utility/Base64.ts)0
-rw-r--r--src/api/util/utility/RandomInviteID.ts (renamed from api/src/util/utility/RandomInviteID.ts)7
-rw-r--r--src/api/util/utility/String.ts (renamed from api/src/util/utility/String.ts)2
-rw-r--r--src/api/util/utility/captcha.ts46
-rw-r--r--src/api/util/utility/ipAddress.ts (renamed from api/src/util/utility/ipAddress.ts)8
-rw-r--r--src/api/util/utility/passwordStrength.ts (renamed from api/src/util/utility/passwordStrength.ts)13
142 files changed, 2227 insertions, 992 deletions
diff --git a/api/src/Server.ts b/src/api/Server.ts

index 4cf0917d..e92335a5 100644 --- a/api/src/Server.ts +++ b/src/api/Server.ts
@@ -1,18 +1,16 @@ -import "missing-native-js-functions"; +import { Config, getOrInitialiseDatabase, initEvent, registerRoutes } from "@fosscord/util"; +import { NextFunction, Request, Response, Router } from "express"; import { Server, ServerOptions } from "lambert-server"; +import morgan from "morgan"; +import path from "path"; +import { red } from "picocolors"; import { Authentication, CORS } from "./middlewares/"; -import { Config, initDatabase, initEvent } from "@fosscord/util"; -import { ErrorHandler } from "./middlewares/ErrorHandler"; import { BodyParser } from "./middlewares/BodyParser"; -import { Router, Request, Response, NextFunction } from "express"; -import path from "path"; +import { ErrorHandler } from "./middlewares/ErrorHandler"; import { initRateLimits } from "./middlewares/RateLimit"; import TestClient from "./middlewares/TestClient"; import { initTranslation } from "./middlewares/Translation"; -import morgan from "morgan"; import { initInstance } from "./util/handlers/Instance"; -import { registerRoutes } from "@fosscord/util"; -import { red } from "picocolors" export interface FosscordServerOptions extends ServerOptions {} @@ -34,7 +32,7 @@ export class FosscordServer extends Server { } async start() { - await initDatabase(); + await getOrInitialiseDatabase(); await Config.init(); await initEvent(); await initInstance(); @@ -44,13 +42,13 @@ export class FosscordServer extends Server { this.app.use( morgan("combined", { skip: (req, res) => { - var skip = !(process.env["LOG_REQUESTS"]?.includes(res.statusCode.toString()) ?? false); + let skip = !(process.env["LOG_REQUESTS"]?.includes(res.statusCode.toString()) ?? false); if (process.env["LOG_REQUESTS"]?.charAt(0) == "-") skip = !skip; return skip; } }) ); - }; + } this.app.use(CORS); this.app.use(BodyParser({ inflate: true, limit: "10mb" })); @@ -87,8 +85,13 @@ export class FosscordServer extends Server { this.app.use(ErrorHandler); TestClient(this.app); - if (logRequests) console.log(red(`Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!`)); - + if (logRequests) + console.log( + red( + `Warning: Request logging is enabled! This will spam your console!\nTo disable this, unset the 'LOG_REQUESTS' environment variable!` + ) + ); + return super.start(); } -}; \ No newline at end of file +} diff --git a/api/src/global.d.ts b/src/api/global.d.ts
index 7751af8f..7751af8f 100644 --- a/api/src/global.d.ts +++ b/src/api/global.d.ts
diff --git a/api/src/index.ts b/src/api/index.ts
index adc7649c..5f97a463 100644 --- a/api/src/index.ts +++ b/src/api/index.ts
@@ -1,3 +1,3 @@ -export * from "./Server"; export * from "./middlewares/"; +export * from "./Server"; export * from "./util/"; diff --git a/api/src/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index 5a08caf3..6d063953 100644 --- a/api/src/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts
@@ -1,15 +1,15 @@ +import { checkToken, Config, HTTPError, Rights } from "@fosscord/util"; import { NextFunction, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; -import { checkToken, Config, Rights } from "@fosscord/util"; export const NO_AUTHORIZATION_ROUTES = [ // Authentication routes "/auth/login", "/auth/register", "/auth/location-metadata", + "/auth/mfa/totp", // Routes with a seperate auth system "/webhooks/", - // Public information endpoints + // Public information endpoints "/ping", "/gateway", "/experiments", diff --git a/api/src/middlewares/BodyParser.ts b/src/api/middlewares/BodyParser.ts
index 4cb376bc..36d89da7 100644 --- a/api/src/middlewares/BodyParser.ts +++ b/src/api/middlewares/BodyParser.ts
@@ -1,6 +1,6 @@ +import { HTTPError } from "@fosscord/util"; import bodyParser, { OptionsJson } from "body-parser"; import { NextFunction, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; export function BodyParser(opts?: OptionsJson) { const jsonParser = bodyParser.json(opts); diff --git a/api/src/middlewares/CORS.ts b/src/api/middlewares/CORS.ts
index 20260cf9..20260cf9 100644 --- a/api/src/middlewares/CORS.ts +++ b/src/api/middlewares/CORS.ts
diff --git a/api/src/middlewares/ErrorHandler.ts b/src/api/middlewares/ErrorHandler.ts
index 2012b91c..813adc18 100644 --- a/api/src/middlewares/ErrorHandler.ts +++ b/src/api/middlewares/ErrorHandler.ts
@@ -1,6 +1,5 @@ +import { ApiError, FieldError, HTTPError } from "@fosscord/util"; import { NextFunction, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; -import { ApiError, FieldError } from "@fosscord/util"; const EntityNotFoundErrorRegex = /"(\w+)"/; export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) { diff --git a/api/src/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts
index 13f1602c..dc93dcef 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/src/api/middlewares/RateLimit.ts
@@ -1,6 +1,6 @@ -import { Config, getRights, listenEvent, Rights } from "@fosscord/util"; -import { NextFunction, Request, Response, Router } from "express"; import { getIpAdress } from "@fosscord/api"; +import { Config, getRights, listenEvent } from "@fosscord/util"; +import { NextFunction, Request, Response, Router } from "express"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; // Docs: https://discord.com/developers/docs/topics/rate-limits @@ -28,7 +28,7 @@ type RateLimit = { expires_at: Date; }; -var Cache = new Map<string, RateLimit>(); +let Cache = new Map<string, RateLimit>(); const EventRateLimit = "RATELIMIT"; export default function rateLimit(opts: { @@ -48,14 +48,14 @@ export default function rateLimit(opts: { // exempt user? if so, immediately short circuit if (req.user_id) { const rights = await getRights(req.user_id); - if (rights.has("BYPASS_RATE_LIMITS")) return; + if (rights.has("BYPASS_RATE_LIMITS")) return next(); } const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); - var executor_id = getIpAdress(req); + let executor_id = getIpAdress(req); if (!opts.onlyIp && req.user_id) executor_id = req.user_id; - var max_hits = opts.count; + let 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; @@ -121,6 +121,7 @@ export default function rateLimit(opts: { export async function initRateLimits(app: Router) { const { routes, global, ip, error, disabled } = Config.get().limits.rate; if (disabled) return; + console.log("Enabling rate limits..."); await listenEvent(EventRateLimit, (event) => { Cache.set(event.channel_id as string, event.data); event.acknowledge?.(); @@ -163,9 +164,9 @@ export async function initRateLimits(app: Router) { app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register })); } -async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number; }) { +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); + let limit = Cache.get(id); if (!limit) { limit = { id: opts.bucket_id, @@ -183,7 +184,7 @@ async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits } /* - var ratelimit = await RateLimit.findOne({ id: opts.bucket_id, executor_id: opts.executor_id }); + let ratelimit = await RateLimit.findOne({ where: { id: opts.bucket_id, executor_id: opts.executor_id } }); if (!ratelimit) { ratelimit = new RateLimit({ id: opts.bucket_id, diff --git a/src/api/middlewares/TestClient.ts b/src/api/middlewares/TestClient.ts new file mode 100644
index 00000000..2c195994 --- /dev/null +++ b/src/api/middlewares/TestClient.ts
@@ -0,0 +1,156 @@ +import { Config } from "@fosscord/util"; +import express, { Application, Request, Response } from "express"; +import fs from "fs"; +import fetch, { Headers, Response as FetchResponse } from "node-fetch"; +import path from "path"; +import { green } from "picocolors"; +import ProxyAgent from "proxy-agent"; +import { AssetCacheItem } from "../util/entities/AssetCacheItem"; + +const AssetsPath = path.join(__dirname, "..", "..", "..", "assets"); + +export default function TestClient(app: Application) { + const agent = new ProxyAgent(); + + //build client page + let html = fs.readFileSync(path.join(AssetsPath, "index.html"), { encoding: "utf8" }); + html = applyEnv(html); + html = applyInlinePlugins(html); + html = applyPlugins(html); + html = applyPreloadPlugins(html); + + //load asset cache + let newAssetCache: Map<string, AssetCacheItem> = new Map<string, AssetCacheItem>(); + let assetCacheDir = path.join(AssetsPath, "cache"); + if (process.env.ASSET_CACHE_DIR) assetCacheDir = process.env.ASSET_CACHE_DIR; + + console.log(`[TestClient] ${green(`Using asset cache path: ${assetCacheDir}`)}`); + if (!fs.existsSync(assetCacheDir)) { + fs.mkdirSync(assetCacheDir); + } + if (fs.existsSync(path.join(assetCacheDir, "index.json"))) { + let rawdata = fs.readFileSync(path.join(assetCacheDir, "index.json")); + newAssetCache = new Map<string, AssetCacheItem>(Object.entries(JSON.parse(rawdata.toString()))); + } + + app.use("/assets", express.static(path.join(AssetsPath))); + app.get("/assets/:file", async (req: Request, res: Response) => { + delete req.headers.host; + let response: FetchResponse; + let buffer: Buffer; + let assetCacheItem: AssetCacheItem = new AssetCacheItem(req.params.file); + if (newAssetCache.has(req.params.file)) { + assetCacheItem = newAssetCache.get(req.params.file)!; + assetCacheItem.Headers.forEach((value: any, name: any) => { + res.set(name, value); + }); + } else { + if(req.params.file.endsWith(".map")) { + return res.status(404).send("Not found"); + } + console.log(`[TestClient] Downloading file not yet cached! Asset file: ${req.params.file}`); + response = await fetch(`https://discord.com/assets/${req.params.file}`, { + agent, + // @ts-ignore + headers: { + ...req.headers + } + }); + + //set cache info + assetCacheItem.Headers = Object.fromEntries(stripHeaders(response.headers)); + assetCacheItem.FilePath = path.join(assetCacheDir, req.params.file); + assetCacheItem.Key = req.params.file; + //add to cache and save + newAssetCache.set(req.params.file, assetCacheItem); + fs.writeFileSync(path.join(assetCacheDir, "index.json"), JSON.stringify(Object.fromEntries(newAssetCache), null, 4)); + //download file + fs.writeFileSync(assetCacheItem.FilePath, await response.buffer()); + } + + assetCacheItem.Headers.forEach((value: string, name: string) => { + res.set(name, value); + }); + return res.send(fs.readFileSync(assetCacheItem.FilePath)); + }); + app.get("/developers*", (_req: Request, res: Response) => { + const { useTestClient } = Config.get().client; + res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24); + res.set("content-type", "text/html"); + + if (!useTestClient) return res.send("Test client is disabled on this instance. Use a stand-alone client to connect this instance."); + + res.send(fs.readFileSync(path.join(__dirname, "..", "..", "..", "assets", "developers.html"), { encoding: "utf8" })); + }); + app.get("*", (req: Request, res: Response) => { + const { useTestClient } = Config.get().client; + res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24); + res.set("content-type", "text/html"); + + if (req.url.startsWith("/api") || req.url.startsWith("/__development")) return; + + if (!useTestClient) return res.send("Test client is disabled on this instance. Use a stand-alone client to connect this instance."); + if (req.url.startsWith("/invite")) return res.send(html.replace("9b2b7f0632acd0c5e781", "9f24f709a3de09b67c49")); + + res.send(html); + }); +} + +function applyEnv(html: string): string { + const CDN_ENDPOINT = (Config.get()?.cdn.endpointPublic || process.env.CDN || "").replace(/(https?)?(:\/\/?)/g, ""); + const GATEWAY_ENDPOINT = Config.get()?.gateway.endpointPublic || process.env.GATEWAY || ""; + + if (CDN_ENDPOINT) { + html = html.replace(/CDN_HOST: .+/, `CDN_HOST: \`${CDN_ENDPOINT}\`,`); + } + if (GATEWAY_ENDPOINT) { + html = html.replace(/GATEWAY_ENDPOINT: .+/, `GATEWAY_ENDPOINT: \`${GATEWAY_ENDPOINT}\`,`); + } + return html; +} + +function applyPlugins(html: string): string { + // plugins + let files = fs.readdirSync(path.join(AssetsPath, "plugins")); + let plugins = ""; + files.forEach((x) => { + if (x.endsWith(".js")) plugins += `<script src='/assets/plugins/${x}'></script>\n`; + }); + return html.replaceAll("<!-- plugin marker -->", plugins); +} + +function applyInlinePlugins(html: string): string { + // inline plugins + let files = fs.readdirSync(path.join(AssetsPath, "inline-plugins")); + let plugins = ""; + files.forEach((x) => { + if (x.endsWith(".js")) plugins += `<script src='/assets/inline-plugins/${x}'></script>\n\n`; + }); + return html.replaceAll("<!-- inline plugin marker -->", plugins); +} + +function applyPreloadPlugins(html: string): string { + //preload plugins + let files = fs.readdirSync(path.join(AssetsPath, "preload-plugins")); + let plugins = ""; + files.forEach((x) => { + if (x.endsWith(".js")) plugins += `<script>${fs.readFileSync(path.join(AssetsPath, "preload-plugins", x))}</script>\n`; + }); + return html.replaceAll("<!-- preload plugin marker -->", plugins); +} + +function stripHeaders(headers: Headers): Headers { + [ + "content-length", + "content-security-policy", + "strict-transport-security", + "set-cookie", + "transfer-encoding", + "expect-ct", + "access-control-allow-origin", + "content-encoding" + ].forEach((headerName) => { + headers.delete(headerName); + }); + return headers; +} diff --git a/api/src/middlewares/Translation.ts b/src/api/middlewares/Translation.ts
index baabf221..8e5e67e6 100644 --- a/api/src/middlewares/Translation.ts +++ b/src/api/middlewares/Translation.ts
@@ -1,13 +1,13 @@ +import { Router } from "express"; import fs from "fs"; -import path from "path"; import i18next from "i18next"; import i18nextMiddleware from "i18next-http-middleware"; import i18nextBackend from "i18next-node-fs-backend"; -import { Router } from "express"; +import path from "path"; export async function initTranslation(router: Router) { - const languages = fs.readdirSync(path.join(__dirname, "..", "..", "locales")); - const namespaces = fs.readdirSync(path.join(__dirname, "..", "..", "locales", "en")); + const languages = fs.readdirSync(path.join(__dirname, "..", "..", "..", "assets", "locales")); + const namespaces = fs.readdirSync(path.join(__dirname, "..", "..", "..", "assets", "locales", "en")); const ns = namespaces.filter((x) => x.endsWith(".json")).map((x) => x.slice(0, x.length - 5)); await i18next @@ -19,7 +19,7 @@ export async function initTranslation(router: Router) { fallbackLng: "en", ns, backend: { - loadPath: __dirname + "/../../locales/{{lng}}/{{ns}}.json" + loadPath: __dirname + "/../../../assets/locales/{{lng}}/{{ns}}.json" }, load: "all" }); diff --git a/api/src/middlewares/index.ts b/src/api/middlewares/index.ts
index f0c50dbe..f0c50dbe 100644 --- a/api/src/middlewares/index.ts +++ b/src/api/middlewares/index.ts
diff --git a/api/src/routes/-/healthz.ts b/src/api/routes/-/healthz.ts
index f7bcfebf..5dee9e86 100644 --- a/api/src/routes/-/healthz.ts +++ b/src/api/routes/-/healthz.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; import { getConnection } from "typeorm"; const router = Router(); diff --git a/api/src/routes/-/readyz.ts b/src/api/routes/-/readyz.ts
index f7bcfebf..5dee9e86 100644 --- a/api/src/routes/-/readyz.ts +++ b/src/api/routes/-/readyz.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; import { getConnection } from "typeorm"; const router = Router(); diff --git a/src/api/routes/applications/#id/bot/index.ts b/src/api/routes/applications/#id/bot/index.ts new file mode 100644
index 00000000..e663059e --- /dev/null +++ b/src/api/routes/applications/#id/bot/index.ts
@@ -0,0 +1,83 @@ +import { route } from "@fosscord/api"; +import { Application, Config, FieldErrors, generateToken, handleFile, OrmUtils, trimSpecial, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +import { verifyToken } from "node-2fa"; + +const router: Router = Router(); + +router.post("/", route({}), async (req: Request, res: Response) => { + const app = await Application.findOne({ where: { id: req.params.id } }); + if (!app) return res.status(404); + const username = trimSpecial(app.name); + const discriminator = await User.generateDiscriminator(username); + if (!discriminator) { + // We've failed to generate a valid and unused discriminator + throw FieldErrors({ + username: { + code: "USERNAME_TOO_MANY_USERS", + message: req?.t("auth:register.USERNAME_TOO_MANY_USERS") + } + }); + } + + const user = OrmUtils.mergeDeep(new User(), { + created_at: new Date(), + username: username, + discriminator, + id: app.id, + bot: true, + system: false, + premium_since: null, + desktop: false, + mobile: false, + premium: false, + premium_type: 0, + bio: app.description, + mfa_enabled: true, + totp_secret: "", + totp_backup_codes: [], + verified: true, + disabled: false, + deleted: false, + email: null, + rights: Config.get().register.defaultRights, + nsfw_allowed: true, + public_flags: "0", + flags: "0", + data: { + hash: null, + valid_tokens_since: new Date() + }, + settings: {}, + extended_settings: {}, + fingerprints: [], + notes: {} + }); + await user.save(); + app.bot = user; + await app.save(); + res.send().status(204); +}); + +router.post("/reset", route({}), async (req: Request, res: Response) => { + let bot = await User.findOne({ where: { id: req.params.id } }); + let owner = await User.findOne({ where: { id: req.user_id } }); + if (!bot) return res.status(404); + if (owner?.totp_secret && (!req.body.code || verifyToken(owner.totp_secret, req.body.code))) { + throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + } + bot.data = { hash: undefined, valid_tokens_since: new Date() }; + await bot.save(); + let token = await generateToken(bot.id); + res.json({ token }).status(200); +}); + +router.patch("/", route({}), async (req: Request, res: Response) => { + if (req.body.avatar) req.body.avatar = await handleFile(`/avatars/${req.params.id}`, req.body.avatar as string); + let app = OrmUtils.mergeDeep(await User.findOne({ where: { id: req.params.id } }), req.body); + await app.save(); + res.json(app).status(200); +}); + +export default router; diff --git a/api/src/routes/applications/#id/entitlements.ts b/src/api/routes/applications/#id/entitlements.ts
index cfcfe40f..26054eb0 100644 --- a/api/src/routes/applications/#id/entitlements.ts +++ b/src/api/routes/applications/#id/entitlements.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/src/api/routes/applications/#id/index.ts b/src/api/routes/applications/#id/index.ts new file mode 100644
index 00000000..398227fd --- /dev/null +++ b/src/api/routes/applications/#id/index.ts
@@ -0,0 +1,29 @@ +import { route } from "@fosscord/api"; +import { Application, OrmUtils } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +const router: Router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + let results = await Application.findOne({ where: { id: req.params.id }, relations: ["owner", "bot"] }); + res.json(results).status(200); +}); + +router.patch("/", route({}), async (req: Request, res: Response) => { + delete req.body.icon; + let app = OrmUtils.mergeDeep(await Application.findOne({ where: { id: req.params.id }, relations: ["owner", "bot"] }), req.body); + if (app.bot) { + app.bot.bio = req.body.description; + app.bot?.save(); + } + if (req.body.tags) app.tags = req.body.tags; + await app.save(); + res.json(app).status(200); +}); + +router.post("/delete", route({}), async (req: Request, res: Response) => { + await Application.delete(req.params.id); + res.send().status(200); +}); + +export default router; diff --git a/api/src/routes/applications/index.ts b/src/api/routes/applications/#id/skus.ts
index 28ce42da..df7ad4bb 100644 --- a/api/src/routes/applications/index.ts +++ b/src/api/routes/applications/#id/skus.ts
@@ -1,11 +1,10 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { - //TODO - res.send([]).status(200); + res.json([]).status(200); }); export default router; diff --git a/api/src/routes/applications/detectable.ts b/src/api/routes/applications/detectable.ts
index 28ce42da..f012a595 100644 --- a/api/src/routes/applications/detectable.ts +++ b/src/api/routes/applications/detectable.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/src/api/routes/applications/index.ts b/src/api/routes/applications/index.ts new file mode 100644
index 00000000..191833f2 --- /dev/null +++ b/src/api/routes/applications/index.ts
@@ -0,0 +1,34 @@ +import { route } from "@fosscord/api"; +import { Application, OrmUtils, trimSpecial, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +const router: Router = Router(); + +export interface ApplicationCreateSchema { + name: string; + team_id?: string | number; +} + +router.get("/", route({}), async (req: Request, res: Response) => { + //TODO + let results = await Application.find({ where: { owner: { id: req.user_id } }, relations: ["owner", "bot"] }); + res.json(results).status(200); +}); + +router.post("/", route({}), async (req: Request, res: Response) => { + const body = req.body as ApplicationCreateSchema; + const user = await User.findOne({ where: { id: req.user_id } }); + if (!user) res.status(420); + let app = OrmUtils.mergeDeep(new Application(), { + name: trimSpecial(body.name), + description: "", + bot_public: true, + owner: user, + verify_key: "IMPLEMENTME", + flags: 0 + }); + await app.save(); + res.json(app).status(200); +}); + +export default router; diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts new file mode 100644
index 00000000..b8caf579 --- /dev/null +++ b/src/api/routes/auth/location-metadata.ts
@@ -0,0 +1,12 @@ +import { getIpAdress, IPAnalysis, route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; +const router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + //TODO + //Note: It's most likely related to legal. At the moment Discord hasn't finished this too + const country_code = (await IPAnalysis(getIpAdress(req))).country_code; + res.json({ consent_required: false, country_code: country_code, promotional_email_opt_in: { required: true, pre_checked: false } }); +}); + +export default router; diff --git a/api/src/routes/auth/login.ts b/src/api/routes/auth/login.ts
index a89721ea..68b2656a 100644 --- a/api/src/routes/auth/login.ts +++ b/src/api/routes/auth/login.ts
@@ -1,30 +1,29 @@ import { Request, Response, Router } from "express"; -import { route } from "@fosscord/api"; -import bcrypt from "bcrypt"; -import { Config, User, generateToken, adjustEmail, FieldErrors } from "@fosscord/util"; +import { route, getIpAdress, verifyCaptcha } from "@fosscord/api"; +import { Config, User, generateToken, adjustEmail, FieldErrors, LoginSchema } from "@fosscord/util"; +import crypto from "crypto"; + +let bcrypt: any; +try { + bcrypt = require("bcrypt"); +} catch { + bcrypt = require("bcryptjs"); + console.log("Warning: using bcryptjs because bcrypt is not installed! Performance will be affected."); +} const router: Router = Router(); export default router; -export interface LoginSchema { - login: string; - password: string; - undelete?: boolean; - captcha_key?: string; - login_source?: string; - gift_code_sku_id?: string; -} - router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Response) => { const { login, password, captcha_key, undelete } = req.body as LoginSchema; const email = adjustEmail(login); - console.log("login", email); + const ip = getIpAdress(req); const config = Config.get(); if (config.login.requireCaptcha && config.security.captcha.enabled) { + const { sitekey, service } = config.security.captcha; if (!captcha_key) { - const { sitekey, service } = config.security.captcha; return res.status(400).json({ captcha_key: ["captcha-required"], captcha_sitekey: sitekey, @@ -32,12 +31,20 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo }); } - // TODO: check captcha + const ip = getIpAdress(req); + const verify = await verifyCaptcha(captcha_key, ip); + if (!verify.success) { + return res.status(400).json({ + captcha_key: verify["error-codes"], + captcha_sitekey: sitekey, + captcha_service: service + }) + } } const user = await User.findOneOrFail({ where: [{ phone: login }, { email: login }], - select: ["data", "id", "disabled", "deleted", "settings"] + select: ["data", "id", "disabled", "deleted", "settings", "totp_secret", "mfa_enabled"] }).catch((e) => { throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } }); }); @@ -57,6 +64,20 @@ router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Respo throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); } + if (user.mfa_enabled) { + // TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy + const ticket = crypto.randomBytes(40).toString("hex"); + + await User.update({ id: user.id }, { totp_last_ticket: ticket }); + + return res.json({ + ticket: ticket, + mfa: true, + sms: false, // TODO + token: null + }); + } + const token = await generateToken(user.id); // Notice this will have a different token structure, than discord diff --git a/src/api/routes/auth/mfa/totp.ts b/src/api/routes/auth/mfa/totp.ts new file mode 100644
index 00000000..9938569e --- /dev/null +++ b/src/api/routes/auth/mfa/totp.ts
@@ -0,0 +1,36 @@ +import { route } from "@fosscord/api"; +import { BackupCode, generateToken, TotpSchema, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +import { verifyToken } from "node-2fa"; +const router = Router(); + +router.post("/", route({ body: "TotpSchema" }), async (req: Request, res: Response) => { + const { code, ticket, gift_code_sku_id, login_source } = req.body as TotpSchema; + + const user = await User.findOneOrFail({ + where: { + totp_last_ticket: ticket + }, + select: ["id", "totp_secret", "settings"] + }); + + const backup = await BackupCode.findOne({ where: { code: code, expired: false, consumed: false, user: { id: user.id } } }); + + if (!backup) { + const ret = verifyToken(user.totp_secret!, code); + if (!ret || ret.delta != 0) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + } else { + backup.consumed = true; + await backup.save(); + } + + await User.update({ id: user.id }, { totp_last_ticket: "" }); + + return res.json({ + token: await generateToken(user.id), + user_settings: user.settings + }); +}); + +export default router; diff --git a/api/src/routes/auth/register.ts b/src/api/routes/auth/register.ts
index 94dd6502..5cc28f7a 100644 --- a/api/src/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts
@@ -1,38 +1,17 @@ +import { getIpAdress, IPAnalysis, isProxy, route, verifyCaptcha } from "@fosscord/api"; +import { adjustEmail, Config, FieldErrors, generateToken, HTTPError, Invite, RegisterSchema, User } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { Config, generateToken, Invite, FieldErrors, User, adjustEmail, trimSpecial } from "@fosscord/util"; -import { route, getIpAdress, IPAnalysis, isProxy } from "@fosscord/api"; -import "missing-native-js-functions"; -import bcrypt from "bcrypt"; -import { HTTPError } from "lambert-server"; -const router: Router = Router(); - -export interface RegisterSchema { - /** - * @minLength 2 - * @maxLength 32 - */ - username: string; - /** - * @minLength 1 - * @maxLength 72 - */ - password?: string; - consent: boolean; - /** - * @TJS-format email - */ - email?: string; - fingerprint?: string; - invite?: string; - /** - * @TJS-type string - */ - date_of_birth?: Date; // "2000-04-03" - gift_code_sku_id?: string; - captcha_key?: string; +let bcrypt: any; +try { + bcrypt = require("bcrypt"); +} catch { + bcrypt = require("bcryptjs"); + console.log("Warning: using bcryptjs because bcrypt is not installed! Performance will be affected."); } +const router: Router = Router(); + router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { const body = req.body as RegisterSchema; const { register, security } = Config.get(); @@ -64,9 +43,15 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } + if (!register.allowGuests) { + throw FieldErrors({ + email: { code: "GUESTS_DISABLED", message: req.t("auth:register.GUESTS_DISABLED") } + }); + } + if (register.requireCaptcha && security.captcha.enabled) { + const { sitekey, service } = security.captcha; if (!body.captcha_key) { - const { sitekey, service } = security.captcha; return res?.status(400).json({ captcha_key: ["captcha-required"], captcha_sitekey: sitekey, @@ -74,7 +59,14 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } - // TODO: check captcha + const verify = await verifyCaptcha(body.captcha_key, ip); + if (!verify.success) { + return res.status(400).json({ + captcha_key: verify["error-codes"], + captcha_sitekey: sitekey, + captcha_service: service + }); + } } if (!register.allowMultipleAccounts) { @@ -108,7 +100,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re } // check if there is already an account with this email - const exists = await User.findOne({ email: email }); + const exists = await User.findOne({ where: { email: email } }); if (exists) { throw FieldErrors({ @@ -124,7 +116,8 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } - if (register.dateOfBirth.required && !body.date_of_birth) { + // If no password is provided, this is a guest account + if (register.dateOfBirth.required && !body.date_of_birth && body.password) { throw FieldErrors({ date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } }); @@ -167,8 +160,6 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re await Invite.joinGuild(user.id, body.invite); } - console.log("register", body.email, body.username, ip); - return res.json({ token: await generateToken(user.id) }); }); diff --git a/api/src/routes/channels/#channel_id/followers.ts b/src/api/routes/channels/#channel_id/followers.ts
index 641af4f8..c06db61b 100644 --- a/api/src/routes/channels/#channel_id/followers.ts +++ b/src/api/routes/channels/#channel_id/followers.ts
@@ -1,4 +1,4 @@ -import { Router, Response, Request } from "express"; +import { Router } from "express"; const router: Router = Router(); // TODO: diff --git a/api/src/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts
index 2fca4fdf..a65cf451 100644 --- a/api/src/routes/channels/#channel_id/index.ts +++ b/src/api/routes/channels/#channel_id/index.ts
@@ -1,15 +1,16 @@ +import { route } from "@fosscord/api"; import { Channel, ChannelDeleteEvent, - ChannelPermissionOverwriteType, + ChannelModifySchema, ChannelType, ChannelUpdateEvent, emitEvent, - Recipient, - handleFile + handleFile, + OrmUtils, + Recipient } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { route } from "@fosscord/api"; const router: Router = Router(); // TODO: delete channel @@ -18,7 +19,7 @@ const router: Router = Router(); router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { channel_id } = req.params; - const channel = await Channel.findOneOrFail({ id: channel_id }); + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); return res.send(channel); }); @@ -29,7 +30,7 @@ router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); if (channel.type === ChannelType.DM) { - const recipient = await Recipient.findOneOrFail({ where: { channel_id: channel_id, user_id: req.user_id } }); + const recipient = await Recipient.findOneOrFail({ where: { channel_id, user_id: req.user_id } }); recipient.closed = true; await Promise.all([ recipient.save(), @@ -47,38 +48,13 @@ router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request res.send(channel); }); -export interface ChannelModifySchema { - /** - * @maxLength 100 - */ - name?: string; - type?: ChannelType; - topic?: string; - icon?: string | null; - bitrate?: number; - user_limit?: number; - rate_limit_per_user?: number; - position?: number; - permission_overwrites?: { - id: string; - type: ChannelPermissionOverwriteType; - allow: string; - deny: string; - }[]; - parent_id?: string; - id?: string; // is not used (only for guild create) - nsfw?: boolean; - rtc_region?: string; - default_auto_archive_duration?: number; -} - router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { - var payload = req.body as ChannelModifySchema; + let payload = req.body as ChannelModifySchema; const { channel_id } = req.params; if (payload.icon) payload.icon = await handleFile(`/channel-icons/${channel_id}`, payload.icon); - const channel = await Channel.findOneOrFail({ id: channel_id }); - channel.assign(payload); + let channel = await Channel.findOneOrFail({ where: { id: channel_id } }); + channel = OrmUtils.mergeDeep(channel, payload); await Promise.all([ channel.save(), diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts new file mode 100644
index 00000000..3a1d2666 --- /dev/null +++ b/src/api/routes/channels/#channel_id/invites.ts
@@ -0,0 +1,58 @@ +import { route } from "@fosscord/api"; +import { Channel, emitEvent, Guild, HTTPError, Invite, InviteCreateEvent, OrmUtils, PublicInviteRelation, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { isTextChannel } from "./messages"; + +const router: Router = Router(); + +router.post( + "/", + route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT_INVITE", right: "CREATE_INVITES" }), + async (req: Request, res: Response) => { + const { user_id } = req; + const { channel_id } = req.params; + const channel = await Channel.findOneOrFail({ where: { id: channel_id }, select: ["id", "name", "type", "guild_id"] }); + isTextChannel(channel.type); + + if (!channel.guild_id) { + throw new HTTPError("This channel doesn't exist", 404); + } + const { guild_id } = channel; + + const expires_at = new Date(req.body.max_age * 1000 + Date.now()); + + const invite = await OrmUtils.mergeDeep(new Invite(), { + temporary: req.body.temporary || true, + max_uses: req.body.max_uses, + max_age: req.body.max_age, + expires_at, + guild_id, + channel_id, + inviter_id: user_id + }).save(); + //TODO: check this, removed toJSON call + const data = JSON.parse(JSON.stringify(invite)); + data.inviter = await User.getPublicUser(req.user_id); + data.guild = await Guild.findOne({ where: { id: guild_id } }); + data.channel = channel; + + await emitEvent({ event: "INVITE_CREATE", data, guild_id } as InviteCreateEvent); + res.status(201).send(data); + } +); + +router.get("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { + const { channel_id } = req.params; + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); + + if (!channel.guild_id) { + throw new HTTPError("This channel doesn't exist", 404); + } + const { guild_id } = channel; + + const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); + + res.status(200).send(invites); +}); + +export default router; diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
index 885c5eca..5ebeed49 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -1,26 +1,17 @@ -import { emitEvent, getPermission, MessageAckEvent, ReadState, Snowflake } from "@fosscord/util"; -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { emitEvent, getPermission, MessageAckEvent, OrmUtils, ReadState } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); -// TODO: public read receipts & privacy scoping -// TODO: send read state event to all channel members -// TODO: advance-only notification cursor - -export interface MessageAcknowledgeSchema { - manual?: boolean; - mention_count?: number; -} - router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; const permission = await getPermission(req.user_id, undefined, channel_id); permission.hasThrow("VIEW_CHANNEL"); - let read_state = await ReadState.findOne({ user_id: req.user_id, channel_id }); - if (!read_state) read_state = new ReadState({ user_id: req.user_id, channel_id }); + let read_state = await ReadState.findOne({ where: { user_id: req.user_id, channel_id } }); + if (!read_state) read_state = OrmUtils.mergeDeep(new ReadState(), { user_id: req.user_id, channel_id }) as ReadState; read_state.last_message_id = message_id; await read_state.save(); diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
index b2cb6763..fbbc65f0 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
index 63fee9b9..b082e083 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -1,25 +1,22 @@ +import { handleMessage, postHandleMessage, route } from "@fosscord/api"; import { Attachment, Channel, - Embed, - DiscordApiErrors, emitEvent, FosscordApiErrors, getPermission, getRights, - Message, + HTTPError, + Message, MessageCreateEvent, + MessageCreateSchema, MessageDeleteEvent, MessageUpdateEvent, Snowflake, - uploadFile + uploadFile } from "@fosscord/util"; -import { Router, Response, Request } from "express"; +import { Request, Response, Router } from "express"; import multer from "multer"; -import { route } from "@fosscord/api"; -import { handleMessage, postHandleMessage } from "@fosscord/api"; -import { MessageCreateSchema } from "../index"; -import { HTTPError } from "lambert-server"; const router = Router(); // TODO: message content/embed string length limit @@ -33,50 +30,53 @@ const messageUpload = multer({ storage: multer.memoryStorage() }); // max upload 50 mb -router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; - var body = req.body as MessageCreateSchema; +router.patch( + "/", + route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), + async (req: Request, res: Response) => { + const { message_id, channel_id } = req.params; + let body = req.body as MessageCreateSchema; - const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] }); + const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] }); - const permissions = await getPermission(req.user_id, undefined, channel_id); - - const rights = await getRights(req.user_id); + const permissions = await getPermission(req.user_id, undefined, channel_id); - if ((req.user_id !== message.author_id)) { - if (!rights.has("MANAGE_MESSAGES")) { - permissions.hasThrow("MANAGE_MESSAGES"); - body = { flags: body.flags }; -// guild admins can only suppress embeds of other messages, no such restriction imposed to instance-wide admins - } - } else rights.hasThrow("SELF_EDIT_MESSAGES"); - - const new_message = await handleMessage({ - ...message, - // TODO: should message_reference be overridable? - // @ts-ignore - message_reference: message.message_reference, - ...body, - author_id: message.author_id, - channel_id, - id: message_id, - edited_timestamp: new Date() - }); - - await Promise.all([ - new_message!.save(), - await emitEvent({ - event: "MESSAGE_UPDATE", + const rights = await getRights(req.user_id); + + if (req.user_id !== message.author_id) { + if (!rights.has("MANAGE_MESSAGES")) { + permissions.hasThrow("MANAGE_MESSAGES"); + body = { flags: body.flags }; + // guild admins can only suppress embeds of other messages, no such restriction imposed to instance-wide admins + } + } else rights.hasThrow("SELF_EDIT_MESSAGES"); + + const new_message = await handleMessage({ + ...message, + // TODO: should message_reference be overridable? + // @ts-ignore + message_reference: message.message_reference, + ...body, + author_id: message.author_id, channel_id, - data: { ...new_message, nonce: undefined } - } as MessageUpdateEvent) - ]); + id: message_id, + edited_timestamp: new Date() + }); - postHandleMessage(message); + await Promise.all([ + new_message!.save(), + await emitEvent({ + event: "MESSAGE_UPDATE", + channel_id, + data: { ...new_message, nonce: undefined } + } as MessageUpdateEvent) + ]); - return res.json(message); -}); + postHandleMessage(message); + return res.json(message); + } +); // Backfill message with specific timestamp router.put( @@ -92,9 +92,9 @@ router.put( route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_BACKDATED_EVENTS" }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; - var body = req.body as MessageCreateSchema; + let body = req.body as MessageCreateSchema; const attachments: Attachment[] = []; - + const rights = await getRights(req.user_id); rights.hasThrow("SEND_MESSAGES"); @@ -103,20 +103,20 @@ router.put( throw new HTTPError("Message IDs must be positive integers", 400); } - const snowflake = Snowflake.deconstruct(message_id) + const snowflake = Snowflake.deconstruct(message_id); if (Date.now() < snowflake.timestamp) { // message is in the future throw FosscordApiErrors.CANNOT_BACKFILL_TO_THE_FUTURE; } - const exists = await Message.findOne({ where: { id: message_id, channel_id: channel_id }}); + const exists = await Message.findOne({ where: { id: message_id, channel_id: channel_id } }); if (exists) { throw FosscordApiErrors.CANNOT_REPLACE_BY_BACKFILL; } if (req.file) { try { - const file = await uploadFile(`/attachments/${req.params.channel_id}`, req.file); + const file: any = await uploadFile(`/attachments/${req.params.channel_id}`, req.file); attachments.push({ ...file, proxy_url: file.url }); } catch (error) { return res.status(400).json(error); @@ -136,19 +136,19 @@ router.put( channel_id, attachments, edited_timestamp: undefined, - timestamp: new Date(snowflake.timestamp), + timestamp: new Date(snowflake.timestamp) }); //Fix for the client bug - delete message.member - + delete message.member; + await Promise.all([ message.save(), emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent), channel.save() ]); - postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error + postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error return res.json(message); } @@ -160,7 +160,7 @@ router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] }); const permissions = await getPermission(req.user_id, undefined, channel_id); - + if (message.author_id !== req.user_id) permissions.hasThrow("READ_MESSAGE_HISTORY"); return res.json(message); @@ -169,12 +169,12 @@ router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: router.delete("/", route({}), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; - const channel = await Channel.findOneOrFail({ id: channel_id }); - const message = await Message.findOneOrFail({ id: message_id }); - + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); + const message = await Message.findOneOrFail({ where: { id: message_id } }); + const rights = await getRights(req.user_id); - if ((message.author_id !== req.user_id)) { + if (message.author_id !== req.user_id) { if (!rights.has("MANAGE_MESSAGES")) { const permission = await getPermission(req.user_id, channel.guild_id, channel_id); permission.hasThrow("MANAGE_MESSAGES"); diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
index d93cf70f..44de5c45 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -1,8 +1,10 @@ +import { route } from "@fosscord/api"; import { Channel, emitEvent, Emoji, getPermission, + HTTPError, Member, Message, MessageReactionAddEvent, @@ -13,9 +15,7 @@ import { PublicUserProjection, User } from "@fosscord/util"; -import { route } from "@fosscord/api"; -import { Router, Response, Request } from "express"; -import { HTTPError } from "lambert-server"; +import { Request, Response, Router } from "express"; import { In } from "typeorm"; const router = Router(); @@ -39,7 +39,7 @@ function getEmoji(emoji: string): PartialEmoji { router.delete("/", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; - const channel = await Channel.findOneOrFail({ id: channel_id }); + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); await Message.update({ id: message_id, channel_id }, { reactions: [] }); @@ -60,7 +60,7 @@ router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: R const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); - const message = await Message.findOneOrFail({ id: message_id, channel_id }); + const message = await Message.findOneOrFail({ where: { 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); @@ -87,7 +87,7 @@ router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); - const message = await Message.findOneOrFail({ id: message_id, channel_id }); + const message = await Message.findOneOrFail({ where: { 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); @@ -101,56 +101,60 @@ router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request res.json(users); }); -router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }), async (req: Request, res: Response) => { - const { message_id, channel_id, user_id } = req.params; - if (user_id !== "@me") throw new HTTPError("Invalid user"); - const emoji = getEmoji(req.params.emoji); - - 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); - - if (!already_added) req.permission!.hasThrow("ADD_REACTIONS"); - - if (emoji.id) { - const external_emoji = await Emoji.findOneOrFail({ id: emoji.id }); - if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS"); - emoji.animated = external_emoji.animated; - emoji.name = external_emoji.name; - } +router.put( + "/:emoji/:user_id", + route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }), + async (req: Request, res: Response) => { + const { message_id, channel_id, user_id } = req.params; + if (user_id !== "@me") throw new HTTPError("Invalid user"); + const emoji = getEmoji(req.params.emoji); + + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); + const message = await Message.findOneOrFail({ where: { 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) req.permission!.hasThrow("ADD_REACTIONS"); + + if (emoji.id) { + const external_emoji = await Emoji.findOneOrFail({ where: { id: emoji.id } }); + if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS"); + emoji.animated = external_emoji.animated; + emoji.name = external_emoji.name; + } - if (already_added) { - if (already_added.user_ids.includes(req.user_id)) return res.sendStatus(204); // Do not throw an error ¯\_(ツ)_/¯ as discord also doesn't throw any error - already_added.count++; - } else message.reactions.push({ count: 1, emoji, user_ids: [req.user_id] }); + if (already_added) { + if (already_added.user_ids.includes(req.user_id)) return res.sendStatus(204); // Do not throw an error ¯\_(ツ)_/¯ as discord also doesn't throw any error + already_added.count++; + } else message.reactions.push({ count: 1, emoji, user_ids: [req.user_id] }); - await message.save(); + await message.save(); - const member = channel.guild_id && (await Member.findOneOrFail({ id: req.user_id })); + const member = channel.guild_id && (await Member.findOneOrFail({ where: { id: req.user_id } })); - await emitEvent({ - event: "MESSAGE_REACTION_ADD", - channel_id, - data: { - user_id: req.user_id, + await emitEvent({ + event: "MESSAGE_REACTION_ADD", channel_id, - message_id, - guild_id: channel.guild_id, - emoji, - member - } - } as MessageReactionAddEvent); + data: { + user_id: req.user_id, + channel_id, + message_id, + guild_id: channel.guild_id, + emoji, + member + } + } as MessageReactionAddEvent); - res.sendStatus(204); -}); + res.sendStatus(204); + } +); router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response) => { - var { message_id, channel_id, user_id } = req.params; + let { message_id, channel_id, user_id } = req.params; const emoji = getEmoji(req.params.emoji); - const channel = await Channel.findOneOrFail({ id: channel_id }); - const message = await Message.findOneOrFail({ id: message_id, channel_id }); + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); + const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } }); if (user_id === "@me") user_id = req.user_id; else { diff --git a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
index 6eacf249..561a40c0 100644 --- a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -1,31 +1,26 @@ -import { Router, Response, Request } from "express"; -import { Channel, Config, emitEvent, getPermission, getRights, MessageDeleteBulkEvent, Message } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; +import { Channel, Config, emitEvent, getPermission, getRights, HTTPError, Message, MessageDeleteBulkEvent } from "@fosscord/util"; +import { Request, Response, Router } from "express"; import { In } from "typeorm"; const router: Router = Router(); export default router; -export interface BulkDeleteSchema { - messages: string[]; -} - // should users be able to bulk delete messages or only bots? ANSWER: all users // should this request fail, if you provide messages older than 14 days/invalid ids? ANSWER: NO // https://discord.com/developers/docs/resources/channel#bulk-delete-messages router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res: Response) => { const { channel_id } = req.params; - const channel = await Channel.findOneOrFail({ id: channel_id }); + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400); const rights = await getRights(req.user_id); rights.hasThrow("SELF_DELETE_MESSAGES"); - + let superuser = rights.has("MANAGE_MESSAGES"); const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); - + const { maxBulkDelete } = Config.get().limits.message; const { messages } = req.body as { messages: string[] }; @@ -35,7 +30,7 @@ router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res: if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`); } - await Message.delete(messages.map((x) => ({ id: x }))); + 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/src/api/routes/channels/#channel_id/messages/index.ts
index 54e6edcc..5fdcb6f9 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -1,22 +1,21 @@ -import { Router, Response, Request } from "express"; +import { handleMessage, postHandleMessage, route } from "@fosscord/api"; import { Attachment, Channel, ChannelType, Config, DmChannelDTO, - Embed, emitEvent, getPermission, - getRights, + HTTPError, + Member, Message, MessageCreateEvent, + MessageCreateSchema, Snowflake, - uploadFile, - Member + uploadFile } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { handleMessage, postHandleMessage, route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; import multer from "multer"; import { FindManyOptions, LessThan, MoreThan } from "typeorm"; import { URL } from "url"; @@ -49,43 +48,11 @@ export function isTextChannel(type: ChannelType): boolean { } } -export interface MessageCreateSchema { - type?: number; - content?: string; - nonce?: string; - channel_id?: string; - tts?: boolean; - flags?: string; - embeds?: Embed[]; - embed?: Embed; - // TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object) - allowed_mentions?: { - parse?: string[]; - roles?: string[]; - users?: string[]; - replied_user?: boolean; - }; - message_reference?: { - message_id: string; - channel_id: string; - guild_id?: string; - fail_if_not_exists?: boolean; - }; - payload_json?: string; - file?: any; - /** - TODO: we should create an interface for attachments - TODO: OpenWAAO<-->attachment-style metadata conversion - **/ - attachments?: any[]; - sticker_ids?: string[]; -} - // https://discord.com/developers/docs/resources/channel#create-message // get messages router.get("/", async (req: Request, res: Response) => { const channel_id = req.params.channel_id; - const channel = await Channel.findOneOrFail({ id: channel_id }); + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); if (!channel) throw new HTTPError("Channel not found", 404); isTextChannel(channel.type); @@ -95,29 +62,26 @@ router.get("/", async (req: Request, res: Response) => { const limit = Number(req.query.limit) || 50; if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100", 422); - var halfLimit = Math.floor(limit / 2); + let halfLimit = Math.floor(limit / 2); const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); permissions.hasThrow("VIEW_CHANNEL"); if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); - var query: FindManyOptions<Message> & { where: { id?: any; }; } = { + let 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) { if (after > new Snowflake()) return res.status(422); query.where.id = MoreThan(after); - } - else if (before) { + } else if (before) { if (before < req.params.channel_id) return res.status(422); query.where.id = LessThan(before); - } - else if (around) { + } else if (around) { query.where.id = [ MoreThan((BigInt(around) - BigInt(halfLimit)).toString()), LessThan((BigInt(around) + BigInt(halfLimit)).toString()) @@ -142,15 +106,14 @@ router.get("/", async (req: Request, res: Response) => { const uri = y.proxy_url.startsWith("http") ? y.proxy_url : `https://example.org${y.proxy_url}`; y.proxy_url = `${endpoint == null ? "" : endpoint}${new URL(uri).pathname}`; }); - + /** Some clients ( discord.js ) only check if a property exists within the response, which causes erorrs when, say, the `application` property is `null`. **/ - - for (var curr in x) { - if (x[curr] === null) - delete x[curr]; + + for (let curr in x) { + if (x[curr] === null) delete x[curr]; } return x; @@ -162,7 +125,7 @@ router.get("/", async (req: Request, res: Response) => { const messageUpload = multer({ limits: { fileSize: 1024 * 1024 * 100, - fields: 10, + fields: 10 // files: 1 }, storage: multer.memoryStorage() @@ -189,21 +152,20 @@ router.post( route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), async (req: Request, res: Response) => { const { channel_id } = req.params; - var body = req.body as MessageCreateSchema; + let body = req.body as MessageCreateSchema; const attachments: Attachment[] = []; const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }); if (!channel.isWritable()) { - throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400) + throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400); } - const files = req.files as Express.Multer.File[] ?? []; - for (var currFile of files) { + const files = (req.files as Express.Multer.File[]) ?? []; + for (let currFile of files) { try { - const file = await uploadFile(`/attachments/${channel.id}`, currFile); + const file: any = await uploadFile(`/attachments/${channel.id}`, currFile); attachments.push({ ...file, proxy_url: file.url }); - } - catch (error) { + } catch (error) { return res.status(400).json(error); } } @@ -244,10 +206,20 @@ router.post( }) ); } - - //Fix for the client bug - delete message.member - + + //Defining member fields + var member = await Member.findOneOrFail({ where: { id: req.user_id }, relations: ["roles"] }); + // TODO: This doesn't work either + // member.roles = member.roles.filter((role) => { + // return role.id !== role.guild_id; + // }).map((role) => { + // return role.id; + // }); + message.member = member; + // TODO: Figure this out + // delete message.member.last_message_id; + // delete message.member.index; + await Promise.all([ message.save(), emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent), @@ -255,9 +227,8 @@ router.post( channel.save() ]); - postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error + postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error return res.json(message); } ); - diff --git a/api/src/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts
index 2eded853..bd462ea6 100644 --- a/api/src/routes/channels/#channel_id/permissions.ts +++ b/src/api/routes/channels/#channel_id/permissions.ts
@@ -1,23 +1,18 @@ +import { route } from "@fosscord/api"; import { Channel, ChannelPermissionOverwrite, - ChannelPermissionOverwriteType, + ChannelPermissionOverwriteSchema, ChannelUpdateEvent, emitEvent, - getPermission, + HTTPError, Member, Role } from "@fosscord/util"; -import { Router, Response, Request } from "express"; -import { HTTPError } from "lambert-server"; +import { Request, Response, Router } from "express"; -import { route } from "@fosscord/api"; const router: Router = Router(); -// TODO: Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel) - -export interface ChannelPermissionOverwriteSchema extends ChannelPermissionOverwrite {} - router.put( "/:overwrite_id", route({ body: "ChannelPermissionOverwriteSchema", permission: "MANAGE_ROLES" }), @@ -25,17 +20,17 @@ router.put( const { channel_id, overwrite_id } = req.params; const body = req.body as ChannelPermissionOverwriteSchema; - var channel = await Channel.findOneOrFail({ id: channel_id }); + let channel = await Channel.findOneOrFail({ where: { id: channel_id } }); if (!channel.guild_id) throw new HTTPError("Channel not found", 404); if (body.type === 0) { - if (!(await Role.count({ id: overwrite_id }))) throw new HTTPError("role not found", 404); + if (!(await Role.count({ where: { id: overwrite_id } }))) throw new HTTPError("role not found", 404); } else if (body.type === 1) { - if (!(await Member.count({ id: overwrite_id }))) throw new HTTPError("user not found", 404); + if (!(await Member.count({ where: { id: overwrite_id } }))) throw new HTTPError("user not found", 404); } else throw new HTTPError("type not supported", 501); // @ts-ignore - var overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id); + let overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id); if (!overwrite) { // @ts-ignore overwrite = { @@ -64,7 +59,7 @@ router.put( router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { const { channel_id, overwrite_id } = req.params; - const channel = await Channel.findOneOrFail({ id: channel_id }); + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); if (!channel.guild_id) throw new HTTPError("Channel not found", 404); channel.permission_overwrites = channel.permission_overwrites!.filter((x) => x.id === overwrite_id); diff --git a/api/src/routes/channels/#channel_id/pins.ts b/src/api/routes/channels/#channel_id/pins.ts
index e71e659f..5c28feac 100644 --- a/api/src/routes/channels/#channel_id/pins.ts +++ b/src/api/routes/channels/#channel_id/pins.ts
@@ -1,28 +1,18 @@ -import { - Channel, - ChannelPinsUpdateEvent, - Config, - emitEvent, - getPermission, - Message, - MessageUpdateEvent, - DiscordApiErrors -} from "@fosscord/util"; -import { Router, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; +import { Channel, ChannelPinsUpdateEvent, Config, DiscordApiErrors, emitEvent, Message, MessageUpdateEvent } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); router.put("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; - const message = await Message.findOneOrFail({ id: message_id }); + const message = await Message.findOneOrFail({ where: { id: message_id } }); // * in dm channels anyone can pin messages -> only check for guilds if (message.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); - const pinned_count = await Message.count({ channel: { id: channel_id }, pinned: true }); + const pinned_count = await Message.count({ where: { channel: { id: channel_id }, pinned: true } }); const { maxPins } = Config.get().limits.channel; if (pinned_count >= maxPins) throw DiscordApiErrors.MAXIMUM_PINS.withParams(maxPins); @@ -50,10 +40,10 @@ router.put("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Re router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; - const channel = await Channel.findOneOrFail({ id: channel_id }); + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); - const message = await Message.findOneOrFail({ id: message_id }); + const message = await Message.findOneOrFail({ where: { id: message_id } }); message.pinned = false; await Promise.all([ @@ -82,7 +72,7 @@ router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: router.get("/", route({ permission: ["READ_MESSAGE_HISTORY"] }), async (req: Request, res: Response) => { const { channel_id } = req.params; - let pins = await Message.find({ channel_id: channel_id, pinned: true }); + let pins = await Message.find({ where: { channel_id, pinned: true } }); res.send(pins); }); diff --git a/src/api/routes/channels/#channel_id/purge.ts b/src/api/routes/channels/#channel_id/purge.ts new file mode 100644
index 00000000..aebdb832 --- /dev/null +++ b/src/api/routes/channels/#channel_id/purge.ts
@@ -0,0 +1,77 @@ +import { route } from "@fosscord/api"; +import { + Channel, + Config, + emitEvent, + getPermission, + getRights, + HTTPError, + Message, + MessageDeleteBulkEvent, + PurgeSchema +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { Between, FindManyOptions, In, Not } from "typeorm"; +import { isTextChannel } from "./messages"; + +const router: Router = Router(); + +export default router; + +/** +TODO: apply the delete bit by bit to prevent client and database stress +**/ +router.post( + "/", + route({ + /*body: "PurgeSchema",*/ + }), + async (req: Request, res: Response) => { + const { channel_id } = req.params; + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); + + if (!channel.guild_id) throw new HTTPError("Can't purge dm channels", 400); + isTextChannel(channel.type); + + const rights = await getRights(req.user_id); + if (!rights.has("MANAGE_MESSAGES")) { + const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); + permissions.hasThrow("MANAGE_MESSAGES"); + permissions.hasThrow("MANAGE_CHANNELS"); + } + + const { before, after } = req.body as PurgeSchema; + + // TODO: send the deletion event bite-by-bite to prevent client stress + + let query: FindManyOptions<Message> & { where: { id?: any } } = { + order: { id: "ASC" }, + // take: limit, + where: { + channel_id, + id: Between(after, before), // the right way around + author_id: rights.has("SELF_DELETE_MESSAGES") ? undefined : Not(req.user_id) + // if you lack the right of self-deletion, you can't delete your own messages, even in purges + }, + relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] + }; + + const messages = await Message.find(query); + const endpoint = Config.get().cdn.endpointPublic; + + if (messages.length == 0) { + res.sendStatus(304); + return; + } + + await Message.delete({ id: In(messages) }); + + await emitEvent({ + event: "MESSAGE_DELETE_BULK", + channel_id, + data: { ids: messages.map((x) => x.id), channel_id, guild_id: channel.guild_id } + } as MessageDeleteBulkEvent); + + res.sendStatus(204); + } +); diff --git a/api/src/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts
index e6466211..276a0eda 100644 --- a/api/src/routes/channels/#channel_id/recipients.ts +++ b/src/api/routes/channels/#channel_id/recipients.ts
@@ -1,4 +1,4 @@ -import { Request, Response, Router } from "express"; +import { route } from "@fosscord/api"; import { Channel, ChannelRecipientAddEvent, @@ -6,11 +6,12 @@ import { DiscordApiErrors, DmChannelDTO, emitEvent, + OrmUtils, PublicUserProjection, Recipient, User } from "@fosscord/util"; -import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); @@ -28,7 +29,7 @@ router.put("/:user_id", route({}), async (req: Request, res: Response) => { throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error? } - channel.recipients!.push(new Recipient({ channel_id: channel_id, user_id: user_id })); + channel.recipients!.push(OrmUtils.mergeDeep(new Recipient(), { channel_id, user_id: user_id })); await channel.save(); await emitEvent({ diff --git a/api/src/routes/channels/#channel_id/typing.ts b/src/api/routes/channels/#channel_id/typing.ts
index 56652368..26d0fcfa 100644 --- a/api/src/routes/channels/#channel_id/typing.ts +++ b/src/api/routes/channels/#channel_id/typing.ts
@@ -1,6 +1,6 @@ -import { Channel, emitEvent, Member, TypingStartEvent } from "@fosscord/util"; import { route } from "@fosscord/api"; -import { Router, Request, Response } from "express"; +import { Channel, emitEvent, Member, TypingStartEvent } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); @@ -8,7 +8,7 @@ router.post("/", route({ permission: "SEND_MESSAGES" }), async (req: Request, re const { channel_id } = req.params; const user_id = req.user_id; const timestamp = Date.now(); - const channel = await Channel.findOneOrFail({ id: channel_id }); + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); const member = await Member.findOne({ where: { id: user_id, guild_id: channel.guild_id }, relations: ["roles", "user"] }); await emitEvent({ diff --git a/api/src/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts
index 92895da6..38dcb869 100644 --- a/api/src/routes/channels/#channel_id/webhooks.ts +++ b/src/api/routes/channels/#channel_id/webhooks.ts
@@ -1,19 +1,9 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; -import { Channel, Config, getPermission, trimSpecial, Webhook } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; +import { Channel, Config, DiscordApiErrors, HTTPError, trimSpecial, Webhook } from "@fosscord/util"; +import { Request, Response, Router } from "express"; import { isTextChannel } from "./messages/index"; -import { DiscordApiErrors } from "@fosscord/util"; const router: Router = Router(); -// TODO: webhooks -export interface WebhookCreateSchema { - /** - * @maxLength 80 - */ - name: string; - avatar: string; -} //TODO: implement webhooks router.get("/", route({}), async (req: Request, res: Response) => { res.json([]); @@ -22,20 +12,21 @@ router.get("/", route({}), async (req: Request, res: Response) => { // TODO: use Image Data Type for avatar instead of String router.post("/", route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), async (req: Request, res: Response) => { const channel_id = req.params.channel_id; - const channel = await Channel.findOneOrFail({ id: channel_id }); + const channel = await Channel.findOneOrFail({ where: { id: channel_id } }); isTextChannel(channel.type); if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400); - const webhook_count = await Webhook.count({ channel_id }); + const webhook_count = await Webhook.count({ where: { channel_id } }); const { maxWebhooks } = Config.get().limits.channel; if (webhook_count > maxWebhooks) throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks); - var { avatar, name } = req.body as { name: string; avatar?: string }; + let { avatar, name } = req.body as { name: string; avatar?: string }; name = trimSpecial(name); if (name === "clyde") throw new HTTPError("Invalid name", 400); // TODO: save webhook in database and send response + res.json(new Webhook()); }); export default router; diff --git a/src/api/routes/discoverable-guilds.ts b/src/api/routes/discoverable-guilds.ts new file mode 100644
index 00000000..2bf49287 --- /dev/null +++ b/src/api/routes/discoverable-guilds.ts
@@ -0,0 +1,39 @@ +import { Config, Guild } from "@fosscord/util"; + +import { Request, Response, Router } from "express"; +import { Like } from "typeorm"; +import { route } from ".."; + +const router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + const { offset, limit, categories } = req.query; + let showAllGuilds = Config.get().guild.discovery.showAllGuilds; + let configLimit = Config.get().guild.discovery.limit; + // ! this only works using SQL querys + // const guilds = await Guild.find({ where: { features: "DISCOVERABLE" } }); //, take: Math.abs(Number(limit)) }); + let guilds; + if (categories == undefined) { + guilds = showAllGuilds + ? await Guild.find({ take: Math.abs(Number(limit || configLimit)) }) + : await Guild.find({ where: { features: Like("%DISCOVERABLE%") }, take: Math.abs(Number(limit || configLimit)) }); + } else { + guilds = showAllGuilds + ? await Guild.find({ where: { primary_category_id: Number(categories) }, take: Math.abs(Number(limit || configLimit)) }) + : await Guild.find({ + where: { primary_category_id: Number(categories), features: Like("%DISCOVERABLE%") }, + take: Math.abs(Number(limit || configLimit)) + }); + } + + const total = guilds ? guilds.length : undefined; + + res.send({ + total: total, + guilds: guilds, + offset: Number(offset || Config.get().guild.discovery.offset), + limit: Number(limit || configLimit) + }); +}); + +export default router; diff --git a/api/src/routes/discovery.ts b/src/api/routes/discovery.ts
index 1991400e..7b9edd48 100644 --- a/api/src/routes/discovery.ts +++ b/src/api/routes/discovery.ts
@@ -1,6 +1,6 @@ import { Categories } from "@fosscord/util"; -import { Router, Response, Request } from "express"; -import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; +import { route } from ".."; const router = Router(); @@ -10,7 +10,7 @@ router.get("/categories", route({}), async (req: Request, res: Response) => { const { locale, primary_only } = req.query; - const out = primary_only ? await Categories.find() : await Categories.find({ where: `"is_primary" = "true"` }); + const out = primary_only ? await Categories.find() : await Categories.find({ where: { is_primary: true } }); res.send(out); }); diff --git a/src/api/routes/downloads.ts b/src/api/routes/downloads.ts new file mode 100644
index 00000000..c86c1fb0 --- /dev/null +++ b/src/api/routes/downloads.ts
@@ -0,0 +1,20 @@ +import { Config, Release } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { route } from ".."; + +const router = Router(); + +router.get("/:branch", route({}), async (req: Request, res: Response) => { + const { client } = Config.get(); + const { branch } = req.params; + const { platform } = req.query; + //TODO + + if (!platform || !["linux", "osx", "win"].includes(platform.toString())) return res.status(404); + + const release = await Release.findOneOrFail({ where: { name: client.releases.upstreamVersion } }); + + res.redirect(release[`win_url`]); +}); + +export default router; diff --git a/api/src/routes/experiments.ts b/src/api/routes/experiments.ts
index 7be86fb8..0355c631 100644 --- a/api/src/routes/experiments.ts +++ b/src/api/routes/experiments.ts
@@ -1,11 +1,11 @@ -import { Router, Response, Request } from "express"; -import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; +import { route } from ".."; const router = Router(); router.get("/", route({}), (req: Request, res: Response) => { // TODO: - res.send({ fingerprint: "", assignments: [], guild_experiments:[] }); + res.send({ fingerprint: "", assignments: [], guild_experiments: [] }); }); export default router; diff --git a/api/src/routes/gateway/bot.ts b/src/api/routes/gateway/bot.ts
index f1dbb9df..0e44f6b2 100644 --- a/api/src/routes/gateway/bot.ts +++ b/src/api/routes/gateway/bot.ts
@@ -1,6 +1,6 @@ -import { Config } from "@fosscord/util"; -import { Router, Response, Request } from "express"; import { route, RouteOptions } from "@fosscord/api"; +import { Config } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/gateway/index.ts b/src/api/routes/gateway/index.ts
index 9bad7478..47037573 100644 --- a/api/src/routes/gateway/index.ts +++ b/src/api/routes/gateway/index.ts
@@ -1,6 +1,6 @@ -import { Config } from "@fosscord/util"; -import { Router, Response, Request } from "express"; import { route, RouteOptions } from "@fosscord/api"; +import { Config } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/gifs/search.ts b/src/api/routes/gifs/search.ts
index 9ad7a592..8b5e984a 100644 --- a/api/src/routes/gifs/search.ts +++ b/src/api/routes/gifs/search.ts
@@ -1,7 +1,7 @@ -import { Router, Response, Request } from "express"; -import fetch from "node-fetch"; -import ProxyAgent from 'proxy-agent'; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; +import fetch from "node-fetch"; +import ProxyAgent from "proxy-agent"; import { getGifApiKey, parseGifResult } from "./trending"; const router = Router(); @@ -11,7 +11,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { const { q, media_format, locale } = req.query; const apiKey = getGifApiKey(); - + const agent = new ProxyAgent(); const response = await fetch(`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`, { @@ -20,7 +20,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { headers: { "Content-Type": "application/json" } }); - const { results } = await response.json(); + const { results } = (await response.json()) as any; res.json(results.map(parseGifResult)).status(200); }); diff --git a/api/src/routes/gifs/trending-gifs.ts b/src/api/routes/gifs/trending-gifs.ts
index 6d97bf7c..65a9600e 100644 --- a/api/src/routes/gifs/trending-gifs.ts +++ b/src/api/routes/gifs/trending-gifs.ts
@@ -1,7 +1,7 @@ -import { Router, Response, Request } from "express"; -import fetch from "node-fetch"; -import ProxyAgent from 'proxy-agent'; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; +import fetch from "node-fetch"; +import ProxyAgent from "proxy-agent"; import { getGifApiKey, parseGifResult } from "./trending"; const router = Router(); @@ -11,7 +11,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { const { media_format, locale } = req.query; const apiKey = getGifApiKey(); - + const agent = new ProxyAgent(); const response = await fetch(`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`, { @@ -20,7 +20,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { headers: { "Content-Type": "application/json" } }); - const { results } = await response.json(); + const { results } = (await response.json()) as any; res.json(results.map(parseGifResult)).status(200); }); diff --git a/api/src/routes/gifs/trending.ts b/src/api/routes/gifs/trending.ts
index c81b4c08..45396ff0 100644 --- a/api/src/routes/gifs/trending.ts +++ b/src/api/routes/gifs/trending.ts
@@ -1,9 +1,8 @@ -import { Router, Response, Request } from "express"; -import fetch from "node-fetch"; -import ProxyAgent from 'proxy-agent'; import { route } from "@fosscord/api"; -import { Config } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; +import { Config, HTTPError } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import fetch from "node-fetch"; +import ProxyAgent from "proxy-agent"; const router = Router(); @@ -34,7 +33,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { const { media_format, locale } = req.query; const apiKey = getGifApiKey(); - + const agent = new ProxyAgent(); const [responseSource, trendGifSource] = await Promise.all([ @@ -50,8 +49,8 @@ router.get("/", route({}), async (req: Request, res: Response) => { }) ]); - const { tags } = await responseSource.json(); - const { results } = await trendGifSource.json(); + const { tags } = (await responseSource.json()) as any; + const { results } = (await trendGifSource.json()) as any; res.json({ categories: tags.map((x: any) => ({ name: x.searchterm, src: x.image })), diff --git a/api/src/routes/guild-recommendations.ts b/src/api/routes/guild-recommendations.ts
index 1432f39c..0248a9c3 100644 --- a/api/src/routes/guild-recommendations.ts +++ b/src/api/routes/guild-recommendations.ts
@@ -1,23 +1,24 @@ -import { Guild, Config } from "@fosscord/util"; +import { Config, Guild } from "@fosscord/util"; -import { Router, Request, Response } from "express"; -import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; +import { Like } from "typeorm"; +import { route } from ".."; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { limit, personalization_disabled } = req.query; - var showAllGuilds = Config.get().guild.discovery.showAllGuilds; + let showAllGuilds = Config.get().guild.discovery.showAllGuilds; // ! this only works using SQL querys // TODO: implement this with default typeorm query // const guilds = await Guild.find({ where: { features: "DISCOVERABLE" } }); //, take: Math.abs(Number(limit)) }); - const genLoadId = (size: Number) => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); + const genLoadId = (size: Number) => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(""); const guilds = showAllGuilds ? await Guild.find({ take: Math.abs(Number(limit || 24)) }) - : await Guild.find({ where: `"features" LIKE '%DISCOVERABLE%'`, take: Math.abs(Number(limit || 24)) }); - res.send({ recommended_guilds: guilds, load_id: `server_recs/${genLoadId(32)}`}).status(200); + : await Guild.find({ where: { features: Like("%DISCOVERABLE%") }, take: Math.abs(Number(limit || 24)) }); + res.send({ recommended_guilds: guilds, load_id: `server_recs/${genLoadId(32)}` }).status(200); }); export default router; diff --git a/api/src/routes/guilds/#guild_id/audit-logs.ts b/src/api/routes/guilds/#guild_id/audit-logs.ts
index a4f2f800..05b9982e 100644 --- a/api/src/routes/guilds/#guild_id/audit-logs.ts +++ b/src/api/routes/guilds/#guild_id/audit-logs.ts
@@ -1,8 +1,5 @@ -import { Router, Response, Request } from "express"; -import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; -import { ChannelModifySchema } from "../../channels/#channel_id"; +import { Request, Response, Router } from "express"; const router = Router(); //TODO: implement audit logs diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts
index 1ce41936..4600b4cb 100644 --- a/api/src/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts
@@ -1,29 +1,18 @@ -import { Request, Response, Router } from "express"; -import { DiscordApiErrors, emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { getIpAdress, route } from "@fosscord/api"; - -export interface BanCreateSchema { - delete_message_days?: string; - reason?: string; -}; - -export interface BanRegistrySchema { - id: string; - user_id: string; - guild_id: string; - executor_id: string; - ip?: string; - reason?: string | undefined; -}; - -export interface BanModeratorSchema { - id: string; - user_id: string; - guild_id: string; - executor_id: string; - reason?: string | undefined; -}; +import { + Ban, + BanModeratorSchema, + BanRegistrySchema, + DiscordApiErrors, + emitEvent, + GuildBanAddEvent, + GuildBanRemoveEvent, + HTTPError, + Member, + OrmUtils, + User +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); @@ -32,7 +21,7 @@ const router: Router = Router(); router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id } = req.params; - let bans = await Ban.find({ guild_id: guild_id }); + let bans = await Ban.find({ where: { guild_id } }); let promisesToAwait: object[] = []; const bansObj: object[] = []; @@ -65,16 +54,16 @@ router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, const { guild_id } = req.params; const user_id = req.params.ban; - let ban = await Ban.findOneOrFail({ guild_id: guild_id, user_id: user_id }) as BanRegistrySchema; - + let ban = (await Ban.findOneOrFail({ where: { guild_id, user_id } })) as BanRegistrySchema; + if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; // pretend self-bans don't exist to prevent victim chasing - + /* Filter secret from registry. */ - + ban = ban as BanModeratorSchema; - delete ban.ip + delete ban.ip; return res.json(ban); }); @@ -83,14 +72,14 @@ router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBER const { guild_id } = req.params; const banned_user_id = req.params.user_id; - if ( (req.user_id === banned_user_id) && (banned_user_id === req.permission!.cache.guild?.owner_id)) + if (req.user_id === banned_user_id && banned_user_id === req.permission!.cache.guild?.owner_id) throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); - + if (req.permission!.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); - + const banned_user = await User.getPublicUser(banned_user_id); - const ban = new Ban({ + const ban = OrmUtils.mergeDeep(new Ban(), { user_id: banned_user_id, guild_id: guild_id, ip: getIpAdress(req), @@ -114,15 +103,15 @@ router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBER return res.json(ban); }); -router.put("/@me", route({ body: "BanCreateSchema"}), async (req: Request, res: Response) => { +router.put("/@me", route({ body: "BanCreateSchema" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const banned_user = await User.getPublicUser(req.params.user_id); - if (req.permission!.cache.guild?.owner_id === req.params.user_id) + if (req.permission!.cache.guild?.owner_id === req.params.user_id) throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); - - const ban = new Ban({ + + const ban = OrmUtils.mergeDeep(new Ban(), { user_id: req.params.user_id, guild_id: guild_id, ip: getIpAdress(req), @@ -149,13 +138,13 @@ router.put("/@me", route({ body: "BanCreateSchema"}), async (req: Request, res: router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id, user_id } = req.params; - let ban = await Ban.findOneOrFail({ guild_id: guild_id, user_id: user_id }); - + let ban = await Ban.findOneOrFail({ where: { guild_id, user_id } }); + if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; // make self-bans irreversible and hide them from view to avoid victim chasing - + const banned_user = await User.getPublicUser(user_id); - + await Promise.all([ Ban.delete({ user_id: user_id, diff --git a/api/src/routes/guilds/#guild_id/channels.ts b/src/api/routes/guilds/#guild_id/channels.ts
index a921fa21..3563eb4c 100644 --- a/api/src/routes/guilds/#guild_id/channels.ts +++ b/src/api/routes/guilds/#guild_id/channels.ts
@@ -1,13 +1,11 @@ -import { Router, Response, Request } from "express"; -import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; -import { ChannelModifySchema } from "../../channels/#channel_id"; +import { Channel, ChannelModifySchema, ChannelReorderSchema, ChannelUpdateEvent, emitEvent, HTTPError } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - const channels = await Channel.find({ guild_id }); + const channels = await Channel.find({ where: { guild_id } }); res.json(channels); }); @@ -22,8 +20,6 @@ router.post("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNE res.status(201).json(channel); }); -export type ChannelReorderSchema = { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[]; - router.patch("/", route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { // changes guild channel position const { guild_id } = req.params; @@ -48,7 +44,7 @@ router.patch("/", route({ body: "ChannelReorderSchema", permission: "MANAGE_CHAN } await Channel.update({ guild_id, id: x.id }, opts); - const channel = await Channel.findOneOrFail({ guild_id, id: x.id }); + const channel = await Channel.findOneOrFail({ where: { guild_id, id: x.id } }); 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/src/api/routes/guilds/#guild_id/delete.ts
index bd158c56..e6a1a6b2 100644 --- a/api/src/routes/guilds/#guild_id/delete.ts +++ b/src/api/routes/guilds/#guild_id/delete.ts
@@ -1,14 +1,13 @@ -import { Channel, emitEvent, GuildDeleteEvent, Guild, Member, Message, Role, Invite, Emoji } from "@fosscord/util"; -import { Router, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; +import { emitEvent, Guild, GuildDeleteEvent, HTTPError } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); // discord prefixes this route with /delete instead of using the delete method // docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild router.post("/", route({}), async (req: Request, res: Response) => { - var { guild_id } = req.params; + let { guild_id } = req.params; 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); diff --git a/src/api/routes/guilds/#guild_id/discovery-requirements.ts b/src/api/routes/guilds/#guild_id/discovery-requirements.ts new file mode 100644
index 00000000..c0260fe7 --- /dev/null +++ b/src/api/routes/guilds/#guild_id/discovery-requirements.ts
@@ -0,0 +1,37 @@ +import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; + +const router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + const { guild_id } = req.params; + // TODO: + // Load from database + // Admin control, but for now it allows anyone to be discoverable + + res.send({ + guild_id: guild_id, + safe_environment: true, + healthy: true, + health_score_pending: false, + size: true, + nsfw_properties: {}, + protected: true, + sufficient: true, + sufficient_without_grace_period: true, + valid_rules_channel: true, + retention_healthy: true, + engagement_healthy: true, + age: true, + minimum_age: 0, + health_score: { + avg_nonnew_participators: 0, + avg_nonnew_communicators: 0, + num_intentful_joiners: 0, + perc_ret_w1_intentful: 0 + }, + minimum_size: 0 + }); +}); + +export default router; diff --git a/api/src/routes/guilds/#guild_id/emojis.ts b/src/api/routes/guilds/#guild_id/emojis.ts
index 85d7ac05..db5ae325 100644 --- a/api/src/routes/guilds/#guild_id/emojis.ts +++ b/src/api/routes/guilds/#guild_id/emojis.ts
@@ -1,21 +1,22 @@ -import { Router, Request, Response } from "express"; -import { Config, DiscordApiErrors, emitEvent, Emoji, GuildEmojisUpdateEvent, handleFile, Member, Snowflake, User } from "@fosscord/util"; import { route } from "@fosscord/api"; +import { + Config, + DiscordApiErrors, + emitEvent, + Emoji, + EmojiCreateSchema, + EmojiModifySchema, + GuildEmojisUpdateEvent, + handleFile, + Member, + OrmUtils, + Snowflake, + User +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); -export interface EmojiCreateSchema { - name?: string; - image: string; - require_colons?: boolean | null; - roles?: string[]; -} - -export interface EmojiModifySchema { - name?: string; - roles?: string[]; -} - router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; @@ -41,16 +42,16 @@ router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_A const body = req.body as EmojiCreateSchema; const id = Snowflake.generate(); - const emoji_count = await Emoji.count({ guild_id: guild_id }); + const emoji_count = await Emoji.count({ where: { guild_id } }); const { maxEmojis } = Config.get().limits.guild; if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis); if (body.require_colons == null) body.require_colons = true; - const user = await User.findOneOrFail({ id: req.user_id }); + const user = await User.findOneOrFail({ where: { id: req.user_id } }); body.image = (await handleFile(`/emojis/${id}`, body.image)) as string; - const emoji = await new Emoji({ + const emoji = await OrmUtils.mergeDeep(new Emoji(), { id: id, guild_id: guild_id, ...body, @@ -66,7 +67,7 @@ router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_A guild_id: guild_id, data: { guild_id: guild_id, - emojis: await Emoji.find({ guild_id: guild_id }) + emojis: await Emoji.find({ where: { guild_id } }) } } as GuildEmojisUpdateEvent); @@ -80,14 +81,14 @@ router.patch( const { emoji_id, guild_id } = req.params; const body = req.body as EmojiModifySchema; - const emoji = await new Emoji({ ...body, id: emoji_id, guild_id: guild_id }).save(); + const emoji = await OrmUtils.mergeDeep(new Emoji(), { ...body, id: emoji_id, guild_id: guild_id }).save(); await emitEvent({ event: "GUILD_EMOJIS_UPDATE", guild_id: guild_id, data: { guild_id: guild_id, - emojis: await Emoji.find({ guild_id: guild_id }) + emojis: await Emoji.find({ where: { guild_id } }) } } as GuildEmojisUpdateEvent); @@ -108,7 +109,7 @@ router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), guild_id: guild_id, data: { guild_id: guild_id, - emojis: await Emoji.find({ guild_id: guild_id }) + emojis: await Emoji.find({ where: { guild_id } }) } } as GuildEmojisUpdateEvent); diff --git a/api/src/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts
index 4ec3df72..af889982 100644 --- a/api/src/routes/guilds/#guild_id/index.ts +++ b/src/api/routes/guilds/#guild_id/index.ts
@@ -1,33 +1,27 @@ -import { Request, Response, Router } from "express"; -import { DiscordApiErrors, emitEvent, getPermission, getRights, Guild, GuildUpdateEvent, handleFile, Member } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; -import "missing-native-js-functions"; -import { GuildCreateSchema } from "../index"; +import { + DiscordApiErrors, + emitEvent, + getPermission, + getRights, + Guild, + GuildUpdateEvent, + GuildUpdateSchema, + handleFile, + HTTPError, + Member, + OrmUtils +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); -export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> { - banner?: string | null; - splash?: string | null; - description?: string; - features?: string[]; - verification_level?: number; - default_message_notifications?: number; - system_channel_flags?: number; - explicit_content_filter?: number; - public_updates_channel_id?: string; - afk_timeout?: number; - afk_channel_id?: string; - preferred_locale?: string; -} - router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const [guild, member] = await Promise.all([ - Guild.findOneOrFail({ id: guild_id }), - Member.findOne({ guild_id: guild_id, id: req.user_id }) + Guild.findOneOrFail({ where: { id: guild_id } }), + Member.findOne({ where: { 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); @@ -37,31 +31,31 @@ router.get("/", route({}), async (req: Request, res: Response) => { return res.send(guild); }); -router.patch("/", route({ body: "GuildUpdateSchema"}), async (req: Request, res: Response) => { +router.patch("/", route({ body: "GuildUpdateSchema" }), async (req: Request, res: Response) => { const body = req.body as GuildUpdateSchema; const { guild_id } = req.params; - - + const rights = await getRights(req.user_id); const permission = await getPermission(req.user_id, guild_id); - - if (!rights.has("MANAGE_GUILDS")||!permission.has("MANAGE_GUILD")) + + if (!rights.has("MANAGE_GUILDS") || !permission.has("MANAGE_GUILD")) throw DiscordApiErrors.MISSING_PERMISSIONS.withParams("MANAGE_GUILD"); - + // TODO: guild update check image if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon); if (body.banner) body.banner = await handleFile(`/banners/${guild_id}`, body.banner); if (body.splash) body.splash = await handleFile(`/splashes/${guild_id}`, body.splash); - var guild = await Guild.findOneOrFail({ + let guild = await Guild.findOneOrFail({ where: { id: guild_id }, relations: ["emojis", "roles", "stickers"] }); // TODO: check if body ids are valid - guild.assign(body); + guild = OrmUtils.mergeDeep(guild, body); - const data = guild.toJSON(); + //TODO: check this, removed toJSON call + const data = JSON.parse(JSON.stringify(guild)); // TODO: guild hashes // TODO: fix vanity_url_code, template_id delete data.vanity_url_code; diff --git a/src/api/routes/guilds/#guild_id/integrations.ts b/src/api/routes/guilds/#guild_id/integrations.ts new file mode 100644
index 00000000..6a5abec3 --- /dev/null +++ b/src/api/routes/guilds/#guild_id/integrations.ts
@@ -0,0 +1,9 @@ +import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; +const router = Router(); + +//TODO: implement integrations list +router.get("/", route({}), async (req: Request, res: Response) => { + res.json([]); +}); +export default router; diff --git a/api/src/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts
index b7534e31..c663df72 100644 --- a/api/src/routes/guilds/#guild_id/invites.ts +++ b/src/api/routes/guilds/#guild_id/invites.ts
@@ -1,5 +1,5 @@ -import { getPermission, Invite, PublicInviteRelation } from "@fosscord/util"; import { route } from "@fosscord/api"; +import { Invite, PublicInviteRelation } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
index c285abb3..57152f9a 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -1,19 +1,26 @@ -import { Request, Response, Router } from "express"; -import { Member, getPermission, getRights, Role, GuildMemberUpdateEvent, emitEvent, Sticker, Emoji, Rights, Guild } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; +import { + emitEvent, + Emoji, + getPermission, + getRights, + Guild, + GuildMemberUpdateEvent, + Member, + MemberChangeSchema, + OrmUtils, + Role, + Sticker +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); -export interface MemberChangeSchema { - roles?: string[]; -} - router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id, member_id } = req.params; await Member.IsInGuildOrFail(req.user_id, guild_id); - const member = await Member.findOneOrFail({ id: member_id, guild_id }); + const member = await Member.findOneOrFail({ where: { id: member_id, guild_id } }); return res.json(member); }); @@ -25,13 +32,13 @@ router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, re const member = await Member.findOneOrFail({ where: { id: member_id, guild_id }, relations: ["roles", "user"] }); const permission = await getPermission(req.user_id, guild_id); - const everyone = await Role.findOneOrFail({ guild_id: guild_id, name: "@everyone", position: 0 }); + const everyone = await Role.findOneOrFail({ where: { guild_id: guild_id, name: "@everyone", position: 0 } }); if (body.roles) { permission.hasThrow("MANAGE_ROLES"); if (body.roles.indexOf(everyone.id) === -1) body.roles.push(everyone.id); - member.roles = body.roles.map((x) => new Role({ id: x })); // foreign key constraint will fail if role doesn't exist + member.roles = body.roles.map((x) => OrmUtils.mergeDeep(new Role(), { id: x })); // foreign key constraint will fail if role doesn't exist } await member.save(); @@ -49,7 +56,6 @@ router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, re }); router.put("/", route({}), async (req: Request, res: Response) => { - // TODO: Lurker mode const rights = await getRights(req.user_id); @@ -59,22 +65,22 @@ router.put("/", route({}), async (req: Request, res: Response) => { member_id = req.user_id; rights.hasThrow("JOIN_GUILDS"); } else { - // TODO: join others by controller + // TODO: join others by controller } - var guild = await Guild.findOneOrFail({ + let guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - var emoji = await Emoji.find({ + let emoji = await Emoji.find({ where: { guild_id: guild_id } }); - var roles = await Role.find({ + let roles = await Role.find({ where: { guild_id: guild_id } }); - var stickers = await Sticker.find({ + let stickers = await Sticker.find({ where: { guild_id: guild_id } }); diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
index 27f7f65d..26411f97 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
@@ -1,16 +1,12 @@ -import { getPermission, Member, PermissionResolvable } from "@fosscord/util"; import { route } from "@fosscord/api"; +import { getPermission, Member, PermissionResolvable } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); -export interface MemberNickChangeSchema { - nick: string; -} - router.patch("/", route({ body: "MemberNickChangeSchema" }), async (req: Request, res: Response) => { - var { guild_id, member_id } = req.params; - var permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; + let { guild_id, member_id } = req.params; + let permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; if (member_id === "@me") { member_id = req.user_id; permissionString = "CHANGE_NICKNAME"; diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
index 8f5ca7ba..0aa7a4dc 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts +++ b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
@@ -1,5 +1,5 @@ -import { getPermission, Member } from "@fosscord/util"; import { route } from "@fosscord/api"; +import { Member } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/guilds/#guild_id/members/index.ts b/src/api/routes/guilds/#guild_id/members/index.ts
index b730a4e7..08164626 100644 --- a/api/src/routes/guilds/#guild_id/members/index.ts +++ b/src/api/routes/guilds/#guild_id/members/index.ts
@@ -1,8 +1,7 @@ -import { Request, Response, Router } from "express"; -import { Guild, Member, PublicMemberProjection } from "@fosscord/util"; import { route } from "@fosscord/api"; +import { HTTPError, Member, PublicMemberProjection } from "@fosscord/util"; +import { Request, Response, Router } from "express"; import { MoreThan } from "typeorm"; -import { HTTPError } from "lambert-server"; const router = Router(); diff --git a/api/src/routes/guilds/#guild_id/premium.ts b/src/api/routes/guilds/#guild_id/premium.ts
index 75361ac6..b7716378 100644 --- a/api/src/routes/guilds/#guild_id/premium.ts +++ b/src/api/routes/guilds/#guild_id/premium.ts
@@ -1,5 +1,5 @@ -import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); router.get("/subscriptions", route({}), async (req: Request, res: Response) => { diff --git a/api/src/routes/guilds/#guild_id/prune.ts b/src/api/routes/guilds/#guild_id/prune.ts
index 0e587d22..3645721c 100644 --- a/api/src/routes/guilds/#guild_id/prune.ts +++ b/src/api/routes/guilds/#guild_id/prune.ts
@@ -1,21 +1,21 @@ -import { Router, Request, Response } from "express"; -import { Guild, Member, Snowflake } from "@fosscord/util"; -import { LessThan, IsNull } from "typeorm"; import { route } from "@fosscord/api"; +import { Guild, Member, Snowflake } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { IsNull, LessThan } from "typeorm"; const router = Router(); //Returns all inactive members, respecting role hierarchy export const inactiveMembers = async (guild_id: string, user_id: string, days: number, roles: string[] = []) => { - var date = new Date(); + let date = new Date(); date.setDate(date.getDate() - days); //Snowflake should have `generateFromTime` method? Or similar? - var minId = BigInt(date.valueOf() - Snowflake.EPOCH) << BigInt(22); + let minId = BigInt(date.valueOf() - Snowflake.EPOCH) << BigInt(22); /** idea: ability to customise the cutoff variable possible candidates: public read receipt, last presence, last VC leave **/ - var members = await Member.find({ + let members = await Member.find({ where: [ { guild_id, @@ -33,7 +33,7 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n //I'm sure I can do this in the above db query ( and it would probably be better to do so ), but oh well. if (roles.length && members.length) members = members.filter((user) => user.roles?.some((role) => roles.includes(role.id))); - const me = await Member.findOneOrFail({ id: user_id, guild_id }, { relations: ["roles"] }); + const me = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["roles"] }); const myHighestRole = Math.max(...(me.roles?.map((x) => x.position) || [])); const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); @@ -54,7 +54,7 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n router.get("/", route({}), async (req: Request, res: Response) => { const days = parseInt(req.query.days as string); - var roles = req.query.include_roles; + let roles = req.query.include_roles; if (typeof roles === "string") roles = [roles]; //express will return array otherwise const members = await inactiveMembers(req.params.guild_id, req.user_id, days, roles as string[]); @@ -62,17 +62,10 @@ router.get("/", route({}), async (req: Request, res: Response) => { res.send({ pruned: members.length }); }); -export interface PruneSchema { - /** - * @min 0 - */ - days: number; -} - router.post("/", route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }), async (req: Request, res: Response) => { const days = parseInt(req.body.days); - var roles = req.query.include_roles; + let roles = req.query.include_roles; if (typeof roles === "string") roles = [roles]; const { guild_id } = req.params; diff --git a/api/src/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts
index 75d24fd1..aa57ec65 100644 --- a/api/src/routes/guilds/#guild_id/regions.ts +++ b/src/api/routes/guilds/#guild_id/regions.ts
@@ -1,13 +1,12 @@ -import { Config, Guild, Member } from "@fosscord/util"; +import { getIpAdress, getVoiceRegions, route } from "@fosscord/api"; +import { Guild } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { getVoiceRegions, route } from "@fosscord/api"; -import { getIpAdress } from "@fosscord/api"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ id: guild_id }); + const guild = await Guild.findOneOrFail({ where: { 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"))); }); diff --git a/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
index 2ad01682..7f9dbc6f 100644 --- a/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts +++ b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
@@ -1,15 +1,23 @@ -import { Router, Request, Response } from "express"; -import { Role, Member, GuildRoleUpdateEvent, GuildRoleDeleteEvent, emitEvent, handleFile } from "@fosscord/util"; import { route } from "@fosscord/api"; -import { HTTPError } from "lambert-server"; -import { RoleModifySchema } from "../"; +import { + emitEvent, + GuildRoleDeleteEvent, + GuildRoleUpdateEvent, + handleFile, + HTTPError, + Member, + OrmUtils, + Role, + RoleModifySchema +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id, role_id } = req.params; await Member.IsInGuildOrFail(req.user_id, guild_id); - const role = await Role.findOneOrFail({ guild_id, id: role_id }); + const role = await Role.findOneOrFail({ where: { guild_id, id: role_id } }); return res.json(role); }); @@ -43,7 +51,7 @@ router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" } if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string); - const role = new Role({ + const role = OrmUtils.mergeDeep(new Role(), { ...body, id: role_id, guild_id, diff --git a/api/src/routes/guilds/#guild_id/roles/index.ts b/src/api/routes/guilds/#guild_id/roles/index.ts
index 53465105..9791f7a9 100644 --- a/api/src/routes/guilds/#guild_id/roles/index.ts +++ b/src/api/routes/guilds/#guild_id/roles/index.ts
@@ -1,43 +1,27 @@ -import { Request, Response, Router } from "express"; +import { route } from "@fosscord/api"; import { - Role, + Config, + DiscordApiErrors, + emitEvent, getPermission, - Member, GuildRoleCreateEvent, GuildRoleUpdateEvent, - GuildRoleDeleteEvent, - emitEvent, - Config, - DiscordApiErrors, - handleFile + Member, + OrmUtils, + Role, + RoleModifySchema, + RolePositionUpdateSchema } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -export interface RoleModifySchema { - name?: string; - permissions?: string; - color?: number; - hoist?: boolean; // whether the role should be displayed separately in the sidebar - mentionable?: boolean; // whether the role should be mentionable - position?: number; - icon?: string; - unicode_emoji?: string; -} - -export type RolePositionUpdateSchema = { - id: string; - position: number; -}[]; - router.get("/", route({}), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; await Member.IsInGuildOrFail(req.user_id, guild_id); - const roles = await Role.find({ guild_id: guild_id }); + const roles = await Role.find({ where: { guild_id } }); return res.json(roles); }); @@ -46,12 +30,12 @@ router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }) const guild_id = req.params.guild_id; const body = req.body as RoleModifySchema; - const role_count = await Role.count({ guild_id }); + const role_count = await Role.count({ where: { guild_id } }); const { maxRoles } = Config.get().limits.guild; if (role_count > maxRoles) throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles); - const role = new Role({ + let role: Role = OrmUtils.mergeDeep(new Role(), { // values before ...body are default and can be overriden position: 0, hoist: false, diff --git a/api/src/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts
index 4ea1dce1..15741780 100644 --- a/api/src/routes/guilds/#guild_id/stickers.ts +++ b/src/api/routes/guilds/#guild_id/stickers.ts
@@ -1,25 +1,26 @@ +import { route } from "@fosscord/api"; import { emitEvent, GuildStickersUpdateEvent, - handleFile, + HTTPError, Member, + ModifyGuildStickerSchema, + OrmUtils, Snowflake, Sticker, StickerFormatType, StickerType, uploadFile } from "@fosscord/util"; -import { Router, Request, Response } from "express"; -import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; import multer from "multer"; -import { HTTPError } from "lambert-server"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; await Member.IsInGuildOrFail(req.user_id, guild_id); - res.json(await Sticker.find({ guild_id })); + res.json(await Sticker.find({ where: { guild_id } })); }); const bodyParser = multer({ @@ -43,7 +44,7 @@ router.post( const id = Snowflake.generate(); const [sticker] = await Promise.all([ - new Sticker({ + OrmUtils.mergeDeep(new Sticker(), { ...body, guild_id, id, @@ -79,25 +80,9 @@ router.get("/:sticker_id", route({}), async (req: Request, res: Response) => { const { guild_id, sticker_id } = req.params; await Member.IsInGuildOrFail(req.user_id, guild_id); - res.json(await Sticker.findOneOrFail({ guild_id, id: sticker_id })); + res.json(await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } })); }); -export interface ModifyGuildStickerSchema { - /** - * @minLength 2 - * @maxLength 30 - */ - name: string; - /** - * @maxLength 100 - */ - description?: string; - /** - * @maxLength 200 - */ - tags: string; -} - router.patch( "/:sticker_id", route({ body: "ModifyGuildStickerSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), @@ -105,7 +90,7 @@ router.patch( const { guild_id, sticker_id } = req.params; const body = req.body as ModifyGuildStickerSchema; - const sticker = await new Sticker({ ...body, guild_id, id: sticker_id }).save(); + const sticker = await OrmUtils.mergeDeep(new Sticker(), { ...body, guild_id, id: sticker_id }).save(); await sendStickerUpdateEvent(guild_id); return res.json(sticker); @@ -118,7 +103,7 @@ async function sendStickerUpdateEvent(guild_id: string) { guild_id: guild_id, data: { guild_id: guild_id, - stickers: await Sticker.find({ guild_id: guild_id }) + stickers: await Sticker.find({ where: { guild_id } }) } } as GuildStickersUpdateEvent); } diff --git a/api/src/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts
index 5179e761..448ee033 100644 --- a/api/src/routes/guilds/#guild_id/templates.ts +++ b/src/api/routes/guilds/#guild_id/templates.ts
@@ -1,8 +1,6 @@ +import { generateCode, route } from "@fosscord/api"; +import { Guild, HTTPError, OrmUtils, Template } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import { Guild, Template } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import { generateCode } from "@fosscord/api"; const router: Router = Router(); @@ -23,20 +21,10 @@ const TemplateGuildProjection: (keyof Guild)[] = [ "icon" ]; -export interface TemplateCreateSchema { - name: string; - description?: string; -} - -export interface TemplateModifySchema { - name: string; - description?: string; -} - router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - var templates = await Template.find({ source_guild_id: guild_id }); + let templates = await Template.find({ where: { source_guild_id: guild_id } }); return res.json(templates); }); @@ -44,10 +32,10 @@ router.get("/", route({}), async (req: Request, res: Response) => { router.post("/", route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); - const exists = await Template.findOneOrFail({ id: guild_id }).catch((e) => {}); + const exists = await Template.findOneOrFail({ where: { id: guild_id } }).catch((e) => {}); if (exists) throw new HTTPError("Template already exists", 400); - const template = await new Template({ + const template = await OrmUtils.mergeDeep(new Template(), { ...req.body, code: generateCode(), creator_id: req.user_id, @@ -75,7 +63,7 @@ router.put("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, const { code, guild_id } = req.params; const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); - const template = await new Template({ code, serialized_source_guild: guild }).save(); + const template = await OrmUtils.mergeDeep(new Template(), { code, serialized_source_guild: guild }).save(); res.json(template); }); @@ -84,7 +72,12 @@ router.patch("/:code", route({ body: "TemplateModifySchema", permission: "MANAGE const { code, guild_id } = req.params; const { name, description } = req.body; - const template = await new Template({ code, name: name, description: description, source_guild_id: guild_id }).save(); + const template = await OrmUtils.mergeDeep(new Template(), { + code, + name: name, + description: description, + source_guild_id: guild_id + }).save(); res.json(template); }); diff --git a/api/src/routes/guilds/#guild_id/vanity-url.ts b/src/api/routes/guilds/#guild_id/vanity-url.ts
index 29cd25e2..bf2db134 100644 --- a/api/src/routes/guilds/#guild_id/vanity-url.ts +++ b/src/api/routes/guilds/#guild_id/vanity-url.ts
@@ -1,7 +1,6 @@ -import { Channel, ChannelType, getPermission, Guild, Invite, trimSpecial } from "@fosscord/util"; -import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; -import { HTTPError } from "lambert-server"; +import { Channel, ChannelType, Guild, HTTPError, Invite, OrmUtils, VanityUrlSchema } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); @@ -9,7 +8,7 @@ const InviteRegex = /\W/g; router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ id: guild_id }); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); if (!guild.features.includes("ALIASABLE_NAMES")) { const invite = await Invite.findOne({ where: { guild_id: guild_id, vanity_url: true } }); @@ -24,30 +23,22 @@ router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: } }); -export interface VanityUrlSchema { - /** - * @minLength 1 - * @maxLength 20 - */ - code?: string; -} - router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const body = req.body as VanityUrlSchema; const code = body.code?.replace(InviteRegex, ""); - const guild = await Guild.findOneOrFail({ id: guild_id }); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); if (!guild.features.includes("VANITY_URL")) throw new HTTPError("Your guild doesn't support vanity urls"); if (!code || code.length === 0) throw new HTTPError("Code cannot be null or empty"); - const invite = await Invite.findOne({ code }); + const invite = await Invite.findOne({ where: { code } }); if (invite) throw new HTTPError("Invite already exists"); - const { id } = await Channel.findOneOrFail({ guild_id, type: ChannelType.GUILD_TEXT }); + const { id } = await Channel.findOneOrFail({ where: { guild_id, type: ChannelType.GUILD_TEXT } }); - await new Invite({ + await OrmUtils.mergeDeep(new Invite(), { vanity_url: true, code: code, temporary: false, @@ -60,7 +51,7 @@ router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }) channel_id: id }).save(); - return res.json({ code: code }); + return res.json({ where: { code } }); }); export default router; diff --git a/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
index f9fbea54..797d348e 100644 --- a/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts +++ b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
@@ -1,23 +1,21 @@ -import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; import { route } from "@fosscord/api"; +import { + Channel, + ChannelType, + DiscordApiErrors, + emitEvent, + getPermission, + OrmUtils, + VoiceState, + VoiceStateUpdateEvent, + VoiceStateUpdateSchema +} from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); -//TODO need more testing when community guild and voice stage channel are working - -export interface VoiceStateUpdateSchema { - channel_id: string; - guild_id?: string; - suppress?: boolean; - request_to_speak_timestamp?: Date; - self_mute?: boolean; - self_deaf?: boolean; - self_video?: boolean; -} - router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request, res: Response) => { const body = req.body as VoiceStateUpdateSchema; - var { guild_id, user_id } = req.params; + let { guild_id, user_id } = req.params; if (user_id === "@me") user_id = req.user_id; const perms = await getPermission(req.user_id, guild_id, body.channel_id); @@ -33,15 +31,17 @@ router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request if (!body.suppress) body.request_to_speak_timestamp = new Date(); if (body.request_to_speak_timestamp) perms.hasThrow("REQUEST_TO_SPEAK"); - const voice_state = await VoiceState.findOne({ - guild_id, - channel_id: body.channel_id, - user_id + let voice_state = await VoiceState.findOne({ + where: { + guild_id, + channel_id: body.channel_id, + user_id + } }); if (!voice_state) throw DiscordApiErrors.UNKNOWN_VOICE_STATE; - voice_state.assign(body); - const channel = await Channel.findOneOrFail({ guild_id, id: body.channel_id }); + voice_state = OrmUtils.mergeDeep(voice_state, body) as VoiceState; + const channel = await Channel.findOneOrFail({ where: { guild_id, id: body.channel_id } }); if (channel.type !== ChannelType.GUILD_STAGE_VOICE) { throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE; } diff --git a/src/api/routes/guilds/#guild_id/webhooks.ts b/src/api/routes/guilds/#guild_id/webhooks.ts new file mode 100644
index 00000000..80e6a59a --- /dev/null +++ b/src/api/routes/guilds/#guild_id/webhooks.ts
@@ -0,0 +1,9 @@ +import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; +const router = Router(); + +//TODO: implement webhooks +router.get("/", route({}), async (req: Request, res: Response) => { + res.json([]); +}); +export default router; diff --git a/api/src/routes/guilds/#guild_id/welcome_screen.ts b/src/api/routes/guilds/#guild_id/welcome_screen.ts
index 7141f17e..85c22a19 100644 --- a/api/src/routes/guilds/#guild_id/welcome_screen.ts +++ b/src/api/routes/guilds/#guild_id/welcome_screen.ts
@@ -1,25 +1,13 @@ -import { Request, Response, Router } from "express"; -import { Guild, getPermission, Snowflake, Member } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; +import { Guild, GuildUpdateWelcomeScreenSchema, HTTPError, Member } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -export interface GuildUpdateWelcomeScreenSchema { - welcome_channels?: { - channel_id: string; - description: string; - emoji_id?: string; - emoji_name: string; - }[]; - enabled?: boolean; - description?: string; -} - router.get("/", route({}), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; - const guild = await Guild.findOneOrFail({ id: guild_id }); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); await Member.IsInGuildOrFail(req.user_id, guild_id); res.json(guild.welcome_screen); @@ -29,7 +17,7 @@ router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "M const guild_id = req.params.guild_id; const body = req.body as GuildUpdateWelcomeScreenSchema; - const guild = await Guild.findOneOrFail({ id: guild_id }); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); if (!guild.welcome_screen.enabled) throw new HTTPError("Welcome screen disabled", 400); if (body.welcome_channels) guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid diff --git a/api/src/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts
index c31519fa..368fe46e 100644 --- a/api/src/routes/guilds/#guild_id/widget.json.ts +++ b/src/api/routes/guilds/#guild_id/widget.json.ts
@@ -1,7 +1,6 @@ -import { Request, Response, Router } from "express"; -import { Config, Permissions, Guild, Invite, Channel, Member } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { random, route } from "@fosscord/api"; +import { Channel, Guild, HTTPError, Invite, Member, OrmUtils, Permissions } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); @@ -17,11 +16,11 @@ const router: Router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ id: guild_id }); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404); // Fetch existing widget invite for widget channel - var invite = await Invite.findOne({ channel_id: guild.widget_channel_id }); + let invite = await Invite.findOne({ where: { channel_id: guild.widget_channel_id } }); if (guild.widget_channel_id && !invite) { // Create invite for channel if none exists @@ -41,7 +40,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { inviter_id: null }; - invite = await new Invite(body).save(); + invite = await OrmUtils.mergeDeep(new Invite(), body).save(); } // Fetch voice channels, and the @everyone permissions object @@ -63,7 +62,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { // Fetch members // TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file) - let members = await Member.find({ guild_id: guild_id }); + let members = await Member.find({ where: { guild_id } }); // Construct object to respond with const data = { diff --git a/api/src/routes/guilds/#guild_id/widget.png.ts b/src/api/routes/guilds/#guild_id/widget.png.ts
index 4c82b740..1c4ef29b 100644 --- a/api/src/routes/guilds/#guild_id/widget.png.ts +++ b/src/api/routes/guilds/#guild_id/widget.png.ts
@@ -1,10 +1,19 @@ -import { Request, Response, Router } from "express"; -import { Guild } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; +import { Guild, HTTPError } from "@fosscord/util"; +import { Request, Response, Router } from "express"; import fs from "fs"; import path from "path"; +// Setup canvas +let createCanvas: any, loadImage: any; +try { + createCanvas = require("canvas").createCanvas; + loadImage = require("canvas").loadImage; +} catch { + console.log("Canvas not found, disabling widgets!"); +} +const sizeOf = require("image-size"); + const router: Router = Router(); // TODO: use svg templates instead of node-canvas for improved performance and to change it easily @@ -12,9 +21,10 @@ const router: Router = Router(); // https://discord.com/developers/docs/resources/guild#get-guild-widget-image // TODO: Cache the response router.get("/", route({}), async (req: Request, res: Response) => { + if (!createCanvas) return res.status(404); const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ id: guild_id }); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404); // Fetch guild information @@ -28,13 +38,8 @@ router.get("/", route({}), async (req: Request, res: Response) => { throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400); } - // Setup canvas - const { createCanvas } = require("canvas"); - const { loadImage } = require("canvas"); - const sizeOf = require("image-size"); - // TODO: Widget style templates need Fosscord branding - const source = path.join(__dirname, "..", "..", "..", "..", "assets", "widget", `${style}.png`); + const source = path.join(__dirname, "..", "..", "..", "..", "..", "assets", "widget", `${style}.png`); if (!fs.existsSync(source)) { throw new HTTPError("Widget template does not exist.", 400); } diff --git a/api/src/routes/guilds/#guild_id/widget.ts b/src/api/routes/guilds/#guild_id/widget.ts
index 2640618d..d2369dd1 100644 --- a/api/src/routes/guilds/#guild_id/widget.ts +++ b/src/api/routes/guilds/#guild_id/widget.ts
@@ -1,11 +1,6 @@ -import { Request, Response, Router } from "express"; -import { Guild } from "@fosscord/util"; import { route } from "@fosscord/api"; - -export interface WidgetModifySchema { - enabled: boolean; // whether the widget is enabled - channel_id: string; // the widget channel id -} +import { Guild, WidgetModifySchema } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); @@ -13,7 +8,7 @@ const router: Router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ id: guild_id }); + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); return res.json({ enabled: guild.widget_enabled || false, channel_id: guild.widget_channel_id || null }); }); diff --git a/api/src/routes/guilds/index.ts b/src/api/routes/guilds/index.ts
index 10721413..6946e2f7 100644 --- a/api/src/routes/guilds/index.ts +++ b/src/api/routes/guilds/index.ts
@@ -1,32 +1,18 @@ -import { Router, Request, Response } from "express"; -import { Role, Guild, Snowflake, Config, getRights, Member, Channel, DiscordApiErrors, handleFile } from "@fosscord/util"; import { route } from "@fosscord/api"; -import { ChannelModifySchema } from "../channels/#channel_id"; +import { Config, DiscordApiErrors, getRights, Guild, GuildCreateSchema, Member } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); -export interface GuildCreateSchema { - /** - * @maxLength 100 - */ - name: string; - region?: string; - icon?: string | null; - channels?: ChannelModifySchema[]; - guild_template_code?: string; - system_channel_id?: string; - rules_channel_id?: string; -} - //TODO: create default channel router.post("/", route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), async (req: Request, res: Response) => { const body = req.body as GuildCreateSchema; const { maxGuilds } = Config.get().limits.user; - const guild_count = await Member.count({ id: req.user_id }); + const guild_count = await Member.count({ where: { id: req.user_id } }); const rights = await getRights(req.user_id); - if ((guild_count >= maxGuilds)&&!rights.has("MANAGE_GUILDS")) { + if (guild_count >= maxGuilds && !rights.has("MANAGE_GUILDS")) { throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); } diff --git a/api/src/routes/guilds/templates/index.ts b/src/api/routes/guilds/templates/index.ts
index 3d922e85..467186a3 100644 --- a/api/src/routes/guilds/templates/index.ts +++ b/src/api/routes/guilds/templates/index.ts
@@ -1,23 +1,18 @@ -import { Request, Response, Router } from "express"; -import { Template, Guild, Role, Snowflake, Config, User, Member } from "@fosscord/util"; import { route } from "@fosscord/api"; -import { DiscordApiErrors } from "@fosscord/util"; +import { Config, DiscordApiErrors, Guild, GuildTemplateCreateSchema, Member, OrmUtils, Role, Snowflake, Template } from "@fosscord/util"; +import { Request, Response, Router } from "express"; import fetch from "node-fetch"; const router: Router = Router(); -export interface GuildTemplateCreateSchema { - name: string; - avatar?: string | null; -} - router.get("/:code", route({}), async (req: Request, res: Response) => { const { allowDiscordTemplates, allowRaws, enabled } = Config.get().templates; if (!enabled) res.json({ code: 403, message: "Template creation & usage is disabled on this instance." }).sendStatus(403); const { code } = req.params; - + if (code.startsWith("discord:")) { - if (!allowDiscordTemplates) return res.json({ code: 403, message: "Discord templates cannot be used on this instance." }).sendStatus(403); + if (!allowDiscordTemplates) + return res.json({ code: 403, message: "Discord templates cannot be used on this instance." }).sendStatus(403); const discordTemplateID = code.split("discord:", 2)[1]; const discordTemplateData = await fetch(`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`, { @@ -28,12 +23,12 @@ router.get("/:code", route({}), async (req: Request, res: Response) => { } if (code.startsWith("external:")) { - if (!allowRaws) return res.json({ code: 403, message: "Importing raws is disabled on this instance." }).sendStatus(403); + if (!allowRaws) return res.json({ code: 403, message: "Importing raws is disabled on this instance." }).sendStatus(403); return res.json(code.split("external:", 2)[1]); } - const template = await Template.findOneOrFail({ code: code }); + const template = await Template.findOneOrFail({ where: { code } }); res.json(template); }); @@ -47,34 +42,36 @@ router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req: const { maxGuilds } = Config.get().limits.user; - const guild_count = await Member.count({ id: req.user_id }); + const guild_count = await Member.count({ where: { id: req.user_id } }); if (guild_count >= maxGuilds) { throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); } - const template = await Template.findOneOrFail({ code: code }); + const template = await Template.findOneOrFail({ where: { code } }); const guild_id = Snowflake.generate(); const [guild, role] = await Promise.all([ - new Guild({ + OrmUtils.mergeDeep(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, - hoist: false, - managed: true, - mentionable: true, - name: "@everyone", - permissions: BigInt("2251804225"), - position: 0, - tags: null - }).save() + ( + OrmUtils.mergeDeep(new Role(), { + id: guild_id, + guild_id: guild_id, + color: 0, + hoist: false, + managed: true, + mentionable: true, + name: "@everyone", + permissions: BigInt("2251804225"), + position: 0, + tags: null + }) as Role + ).save() ]); await Member.addToGuild(req.user_id, guild_id); diff --git a/api/src/routes/invites/index.ts b/src/api/routes/invites/index.ts
index eeafb22a..73c9324c 100644 --- a/api/src/routes/invites/index.ts +++ b/src/api/routes/invites/index.ts
@@ -1,7 +1,6 @@ -import { Router, Request, Response } from "express"; -import { emitEvent, getPermission, Guild, Invite, InviteDeleteEvent, User, PublicInviteRelation } from "@fosscord/util"; import { route } from "@fosscord/api"; -import { HTTPError } from "lambert-server"; +import { emitEvent, getPermission, Guild, HTTPError, Invite, InviteDeleteEvent, PublicInviteRelation, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); @@ -13,15 +12,16 @@ router.get("/:code", route({}), async (req: Request, res: Response) => { res.status(200).send(invite); }); -router.post("/:code", route({right: "USE_MASS_INVITES"}), async (req: Request, res: Response) => { +router.post("/:code", route({ right: "USE_MASS_INVITES" }), async (req: Request, res: Response) => { const { code } = req.params; - const { guild_id } = await Invite.findOneOrFail({ code }) - const { features } = await Guild.findOneOrFail({ id: guild_id}); - const { public_flags } = await User.findOneOrFail({ id: req.user_id }); - - if(features.includes("INTERNAL_EMPLOYEE_ONLY") && (public_flags & 1) !== 1) throw new HTTPError("Only intended for the staff of this server.", 401); - if(features.includes("INVITES_CLOSED")) throw new HTTPError("Sorry, this guild has joins closed.", 403); - + const { guild_id } = await Invite.findOneOrFail({ where: { code } }); + const { features } = await Guild.findOneOrFail({ where: { id: guild_id } }); + const { public_flags } = await User.findOneOrFail({ where: { id: req.user_id } }); + + if (features.includes("INTERNAL_EMPLOYEE_ONLY") && (public_flags & 1) !== 1) + throw new HTTPError("Only intended for the staff of this server.", 401); + if (features.includes("INVITES_CLOSED")) throw new HTTPError("Sorry, this guild has joins closed.", 403); + const invite = await Invite.joinGuild(req.user_id, code); res.json(invite); @@ -30,7 +30,7 @@ router.post("/:code", route({right: "USE_MASS_INVITES"}), async (req: Request, r // * cant use permission of route() function because path doesn't have guild_id/channel_id router.delete("/:code", route({}), async (req: Request, res: Response) => { const { code } = req.params; - const invite = await Invite.findOneOrFail({ code }); + const invite = await Invite.findOneOrFail({ where: { code } }); const { guild_id, channel_id } = invite; const permission = await getPermission(req.user_id, guild_id, channel_id); diff --git a/api/src/routes/oauth2/tokens.ts b/src/api/routes/oauth2/tokens.ts
index bd284221..831dc7af 100644 --- a/api/src/routes/oauth2/tokens.ts +++ b/src/api/routes/oauth2/tokens.ts
@@ -1,5 +1,5 @@ -import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { diff --git a/api/src/routes/outbound-promotions.ts b/src/api/routes/outbound-promotions.ts
index 411e95bf..8e407184 100644 --- a/api/src/routes/outbound-promotions.ts +++ b/src/api/routes/outbound-promotions.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/src/api/routes/partners/#guild_id/requirements.ts b/src/api/routes/partners/#guild_id/requirements.ts new file mode 100644
index 00000000..c0260fe7 --- /dev/null +++ b/src/api/routes/partners/#guild_id/requirements.ts
@@ -0,0 +1,37 @@ +import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; + +const router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + const { guild_id } = req.params; + // TODO: + // Load from database + // Admin control, but for now it allows anyone to be discoverable + + res.send({ + guild_id: guild_id, + safe_environment: true, + healthy: true, + health_score_pending: false, + size: true, + nsfw_properties: {}, + protected: true, + sufficient: true, + sufficient_without_grace_period: true, + valid_rules_channel: true, + retention_healthy: true, + engagement_healthy: true, + age: true, + minimum_age: 0, + health_score: { + avg_nonnew_participators: 0, + avg_nonnew_communicators: 0, + num_intentful_joiners: 0, + perc_ret_w1_intentful: 0 + }, + minimum_size: 0 + }); +}); + +export default router; diff --git a/api/src/routes/ping.ts b/src/api/routes/ping.ts
index 3c1da2c3..5f1b0174 100644 --- a/api/src/routes/ping.ts +++ b/src/api/routes/ping.ts
@@ -1,6 +1,6 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; import { Config } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); @@ -18,8 +18,8 @@ router.get("/", route({}), (req: Request, res: Response) => { correspondenceUserID: general.correspondenceUserID, frontPage: general.frontPage, - tosPage: general.tosPage, - }, + tosPage: general.tosPage + } }); }); diff --git a/src/api/routes/policies/instance/domains.ts b/src/api/routes/policies/instance/domains.ts new file mode 100644
index 00000000..fed0a627 --- /dev/null +++ b/src/api/routes/policies/instance/domains.ts
@@ -0,0 +1,17 @@ +import { route } from "@fosscord/api"; +import { Config } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +const router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + const { cdn, gateway } = Config.get(); + + const IdentityForm = { + cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001", + gateway: gateway.endpointPublic || process.env.GATEWAY || "ws://localhost:3002" + }; + + res.json(IdentityForm); +}); + +export default router; diff --git a/api/src/routes/policies/instance/index.ts b/src/api/routes/policies/instance/index.ts
index e3da014f..a8ffd285 100644 --- a/api/src/routes/policies/instance/index.ts +++ b/src/api/routes/policies/instance/index.ts
@@ -1,10 +1,9 @@ -import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; import { Config } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); - -router.get("/",route({}), async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { general } = Config.get(); res.json(general); }); diff --git a/api/src/routes/policies/instance/limits.ts b/src/api/routes/policies/instance/limits.ts
index 7de1476b..0d42fc7b 100644 --- a/api/src/routes/policies/instance/limits.ts +++ b/src/api/routes/policies/instance/limits.ts
@@ -1,9 +1,9 @@ -import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; import { Config } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); -router.get("/",route({}), async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { limits } = Config.get(); res.json(limits); }); diff --git a/src/api/routes/scheduled-maintenances/upcoming_json.ts b/src/api/routes/scheduled-maintenances/upcoming_json.ts new file mode 100644
index 00000000..ec4ddc7c --- /dev/null +++ b/src/api/routes/scheduled-maintenances/upcoming_json.ts
@@ -0,0 +1,12 @@ +import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; +const router = Router(); + +router.get("/scheduled-maintenances/upcoming.json", route({}), async (req: Request, res: Response) => { + res.json({ + page: {}, + scheduled_maintenances: {} + }); +}); + +export default router; diff --git a/api/src/routes/science.ts b/src/api/routes/science.ts
index 8556a3ad..cb01e576 100644 --- a/api/src/routes/science.ts +++ b/src/api/routes/science.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/stage-instances.ts b/src/api/routes/stage-instances.ts
index 411e95bf..8e407184 100644 --- a/api/src/routes/stage-instances.ts +++ b/src/api/routes/stage-instances.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/sticker-packs/index.ts b/src/api/routes/sticker-packs/index.ts
index e6560d12..dddc7f70 100644 --- a/api/src/routes/sticker-packs/index.ts +++ b/src/api/routes/sticker-packs/index.ts
@@ -1,6 +1,6 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; import { StickerPack } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/stickers/#sticker_id/index.ts b/src/api/routes/stickers/#sticker_id/index.ts
index 293ca089..16eb2059 100644 --- a/api/src/routes/stickers/#sticker_id/index.ts +++ b/src/api/routes/stickers/#sticker_id/index.ts
@@ -1,12 +1,12 @@ -import { Sticker } from "@fosscord/util"; -import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; +import { Sticker } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); router.get("/", route({}), async (req: Request, res: Response) => { const { sticker_id } = req.params; - res.json(await Sticker.find({ id: sticker_id })); + res.json(await Sticker.find({ where: { id: sticker_id } })); }); export default router; diff --git a/api/src/routes/stop.ts b/src/api/routes/stop.ts
index 7f8b78ba..fb77b4f3 100644 --- a/api/src/routes/stop.ts +++ b/src/api/routes/stop.ts
@@ -1,22 +1,21 @@ -import { Router, Request, Response } from "express"; import { route } from "@fosscord/api"; import { User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); router.post("/", route({}), async (req: Request, res: Response) => { //EXPERIMENTAL: have an "OPERATOR" platform permission implemented for this API route const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["rights"] }); - if((Number(user.rights) << Number(0))%Number(2)==Number(1)) { + if ((Number(user.rights) << Number(0)) % Number(2) == Number(1)) { console.log("user that POSTed to the API was ALLOWED"); console.log(user.rights); - res.sendStatus(200) - process.kill(process.pid, 'SIGTERM') - } - else { + res.sendStatus(200); + process.kill(process.pid, "SIGTERM"); + } else { console.log("operation failed"); console.log(user.rights); - res.sendStatus(403) + res.sendStatus(403); } }); diff --git a/api/src/routes/store/published-listings/applications.ts b/src/api/routes/store/published-listings/applications.ts
index 060a4c3d..3d0f7998 100644 --- a/api/src/routes/store/published-listings/applications.ts +++ b/src/api/routes/store/published-listings/applications.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/store/published-listings/applications/#id/subscription-plans.ts b/src/api/routes/store/published-listings/applications/#id/subscription-plans.ts
index 54151ae5..86fce75d 100644 --- a/api/src/routes/store/published-listings/applications/#id/subscription-plans.ts +++ b/src/api/routes/store/published-listings/applications/#id/subscription-plans.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/store/published-listings/skus.ts b/src/api/routes/store/published-listings/skus.ts
index 060a4c3d..3d0f7998 100644 --- a/api/src/routes/store/published-listings/skus.ts +++ b/src/api/routes/store/published-listings/skus.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts b/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts new file mode 100644
index 00000000..f8c78bc5 --- /dev/null +++ b/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
@@ -0,0 +1,313 @@ +import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; + +const router: Router = Router(); + +const skus = new Map([ + [ + "521842865731534868", + [ + { + id: "511651856145973248", + name: "Individual Premium Tier 3 Monthly (Legacy)", + interval: 1, + interval_count: 1, + tax_inclusive: true, + sku_id: "521842865731534868", + currency: "eur", + price: 0, + price_tier: null + }, + { + id: "511651860671627264", + name: "Individiual Premium Tier 3 Yearly (Legacy)", + interval: 2, + interval_count: 1, + tax_inclusive: true, + sku_id: "521842865731534868", + currency: "eur", + price: 0, + price_tier: null + } + ] + ], + [ + "521846918637420545", + [ + { + id: "511651871736201216", + name: "Individual Premium Tier 2 Monthly", + interval: 1, + interval_count: 1, + tax_inclusive: true, + sku_id: "521846918637420545", + currency: "eur", + price: 0, + price_tier: null + }, + { + id: "511651876987469824", + name: "Individual Premum Tier 2 Yearly", + interval: 2, + interval_count: 1, + tax_inclusive: true, + sku_id: "521846918637420545", + currency: "eur", + price: 0, + price_tier: null + }, + { + id: "978380684370378761", + name: "Individual Premum Tier 1", + interval: 2, + interval_count: 1, + tax_inclusive: true, + sku_id: "521846918637420545", + currency: "eur", + price: 0, + price_tier: null + } + ] + ], + [ + "521847234246082599", + [ + { + id: "642251038925127690", + name: "Individual Premium Tier 3 Quarterly", + interval: 1, + interval_count: 3, + tax_inclusive: true, + sku_id: "521847234246082599", + currency: "eur", + price: 0, + price_tier: null + }, + { + id: "511651880837840896", + name: "Individual Premium Tier 3 Monthly", + interval: 1, + interval_count: 1, + tax_inclusive: true, + sku_id: "521847234246082599", + currency: "eur", + price: 0, + price_tier: null + }, + { + id: "511651885459963904", + name: "Individual Premium Tier 3 Yearly", + interval: 2, + interval_count: 1, + tax_inclusive: true, + sku_id: "521847234246082599", + currency: "eur", + price: 0, + price_tier: null + } + ] + ], + [ + "590663762298667008", + [ + { + id: "590665532894740483", + name: "Crowd Premium Monthly", + interval: 1, + interval_count: 1, + tax_inclusive: true, + sku_id: "590663762298667008", + discount_price: 0, + currency: "eur", + price: 0, + price_tier: null + }, + { + id: "590665538238152709", + name: "Crowd Premium Yearly", + interval: 2, + interval_count: 1, + tax_inclusive: true, + sku_id: "590663762298667008", + discount_price: 0, + currency: "eur", + price: 0, + price_tier: null + } + ], + ], + [ + "978380684370378762", + [ + [ + { + "id": "978380692553465866", + "name": "Premium Tier 0 Monthly", + "interval": 1, + "interval_count": 1, + "tax_inclusive": true, + "sku_id": "978380684370378762", + "currency": "usd", + "price": 299, + "price_tier": null, + "prices": { + "0": { + "country_prices": { + "country_code": "US", + "prices": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ] + }, + "payment_source_prices": { + "775487223059316758": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ], + "736345864146255982": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ], + "683074999590060249": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ] + } + }, + "3": { + "country_prices": { + "country_code": "US", + "prices": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ] + }, + "payment_source_prices": { + "775487223059316758": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ], + "736345864146255982": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ], + "683074999590060249": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ] + } + }, + "4": { + "country_prices": { + "country_code": "US", + "prices": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ] + }, + "payment_source_prices": { + "775487223059316758": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ], + "736345864146255982": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ], + "683074999590060249": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ] + } + }, + "1": { + "country_prices": { + "country_code": "US", + "prices": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ] + }, + "payment_source_prices": { + "775487223059316758": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ], + "736345864146255982": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ], + "683074999590060249": [ + { + "currency": "usd", + "amount": 0, + "exponent": 2 + } + ] + } + } + } + } + ] + ] + ] +]); + +router.get("/", route({}), async (req: Request, res: Response) => { + // TODO: add the ability to add custom + const { sku_id } = req.params; + + if (!skus.has(sku_id)) { + console.log(`Request for invalid SKU ${sku_id}! Please report this!`); + res.sendStatus(404); + } else { + res.json(skus.get(sku_id)).status(200); + } +}); + +export default router; diff --git a/api/src/routes/teams.ts b/src/api/routes/teams.ts
index 7ce3abcb..9aa1c10e 100644 --- a/api/src/routes/teams.ts +++ b/src/api/routes/teams.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/template.ts.disabled b/src/api/routes/template.ts.disabled
index fcc59ef4..fcc59ef4 100644 --- a/api/src/routes/template.ts.disabled +++ b/src/api/routes/template.ts.disabled
diff --git a/api/src/routes/track.ts b/src/api/routes/track.ts
index 8556a3ad..cb01e576 100644 --- a/api/src/routes/track.ts +++ b/src/api/routes/track.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/src/api/routes/updates.ts b/src/api/routes/updates.ts new file mode 100644
index 00000000..6019371e --- /dev/null +++ b/src/api/routes/updates.ts
@@ -0,0 +1,20 @@ +import { route } from "@fosscord/api"; +import { Config, Release } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +const router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + const { client } = Config.get(); + + const release = await Release.findOneOrFail({ where: { name: client.releases.upstreamVersion } }); + + res.json({ + name: release.name, + pub_date: release.pub_date, + url: release.url, + notes: release.notes + }); +}); + +export default router; diff --git a/api/src/routes/users/#id/index.ts b/src/api/routes/users/#id/index.ts
index bdb1060f..e33e5695 100644 --- a/api/src/routes/users/#id/index.ts +++ b/src/api/routes/users/#id/index.ts
@@ -1,6 +1,6 @@ -import { Router, Request, Response } from "express"; -import { User } from "@fosscord/util"; import { route } from "@fosscord/api"; +import { User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts new file mode 100644
index 00000000..766c9880 --- /dev/null +++ b/src/api/routes/users/#id/profile.ts
@@ -0,0 +1,94 @@ +import { route } from "@fosscord/api"; +import { Member, PublicConnectedAccount, User, UserPublic } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +const router: Router = Router(); + +export interface UserProfileResponse { + user: UserPublic; + connected_accounts: PublicConnectedAccount; + premium_guild_since?: Date; + premium_since?: Date; +} + +router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => { + if (req.params.id === "@me") req.params.id = req.user_id; + + const { guild_id, with_mutual_guilds } = req.query; + + const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] }); + + var mutual_guilds: object[] = []; + var premium_guild_since; + + if (with_mutual_guilds == "true") { + const requested_member = await Member.find({ where: { id: req.params.id } }); + const self_member = await Member.find({ where: { id: req.user_id } }); + + for (const rmem of requested_member) { + if (rmem.premium_since) { + if (premium_guild_since) { + if (premium_guild_since > rmem.premium_since) { + premium_guild_since = rmem.premium_since; + } + } else { + premium_guild_since = rmem.premium_since; + } + } + for (const smem of self_member) { + if (smem.guild_id === rmem.guild_id) { + mutual_guilds.push({ id: rmem.guild_id, nick: rmem.nick }); + } + } + } + } + + const guild_member = + guild_id && typeof guild_id == "string" + ? await Member.findOneOrFail({ where: { id: req.params.id, guild_id: guild_id }, relations: ["roles"] }) + : undefined; + + // TODO: make proper DTO's in util? + + const userDto = { + 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 + }; + + const guildMemberDto = guild_member + ? { + avatar: user.avatar, // TODO + banner: user.banner, // TODO + bio: req.user_bot ? null : user.bio, // TODO + communication_disabled_until: null, // TODO + deaf: guild_member.deaf, + flags: user.flags, + is_pending: guild_member.pending, + pending: guild_member.pending, // why is this here twice, discord? + joined_at: guild_member.joined_at, + mute: guild_member.mute, + nick: guild_member.nick, + premium_since: guild_member.premium_since, + roles: guild_member.roles.map((x) => x.id).filter((id) => id != guild_id), + user: userDto + } + : undefined; + + res.json({ + connected_accounts: user.connected_accounts, + premium_guild_since: premium_guild_since, // TODO + premium_since: user.premium_since, // TODO + mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true + user: userDto, + guild_member: guildMemberDto + }); +}); + +export default router; diff --git a/src/api/routes/users/#id/relationships.ts b/src/api/routes/users/#id/relationships.ts new file mode 100644
index 00000000..9b7e3402 --- /dev/null +++ b/src/api/routes/users/#id/relationships.ts
@@ -0,0 +1,46 @@ +import { route } from "@fosscord/api"; +import { User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +const router: Router = Router(); + +export interface UserRelationsResponse { + object: { + id?: string; + username?: string; + avatar?: string; + discriminator?: string; + public_flags?: number; + }; +} + +router.get("/", route({ test: { response: { body: "UserRelationsResponse" } } }), async (req: Request, res: Response) => { + let mutual_relations: object[] = []; + const requested_relations = await User.findOneOrFail({ + where: { id: req.params.id }, + relations: ["relationships"] + }); + const self_relations = await User.findOneOrFail({ + where: { id: req.user_id }, + relations: ["relationships"] + }); + + for (const rmem of requested_relations.relationships) { + for (const smem of self_relations.relationships) + if (rmem.to_id === smem.to_id && rmem.type === 1 && rmem.to_id !== req.user_id) { + let relation_user = await User.getPublicUser(rmem.to_id); + + mutual_relations.push({ + id: relation_user.id, + username: relation_user.username, + avatar: relation_user.avatar, + discriminator: relation_user.discriminator, + public_flags: relation_user.public_flags + }); + } + } + + res.json(mutual_relations); +}); + +export default router; diff --git a/api/src/routes/users/@me/activities/statistics/applications.ts b/src/api/routes/users/@me/activities/statistics/applications.ts
index 014df8af..ba359b47 100644 --- a/api/src/routes/users/@me/activities/statistics/applications.ts +++ b/src/api/routes/users/@me/activities/statistics/applications.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/users/@me/affinities/guilds.ts b/src/api/routes/users/@me/affinities/guilds.ts
index 8d744744..e733910f 100644 --- a/api/src/routes/users/@me/affinities/guilds.ts +++ b/src/api/routes/users/@me/affinities/guilds.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/users/@me/affinities/users.ts b/src/api/routes/users/@me/affinities/users.ts
index 6d4e4991..758bedc3 100644 --- a/api/src/routes/users/@me/affinities/users.ts +++ b/src/api/routes/users/@me/affinities/users.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/users/@me/applications/#app_id/entitlements.ts b/src/api/routes/users/@me/applications/#app_id/entitlements.ts
index 411e95bf..8e407184 100644 --- a/api/src/routes/users/@me/applications/#app_id/entitlements.ts +++ b/src/api/routes/users/@me/applications/#app_id/entitlements.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/users/@me/billing/country-code.ts b/src/api/routes/users/@me/billing/country-code.ts
index 33d40796..72601f42 100644 --- a/api/src/routes/users/@me/billing/country-code.ts +++ b/src/api/routes/users/@me/billing/country-code.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/users/@me/billing/payment-sources.ts b/src/api/routes/users/@me/billing/payment-sources.ts
index 014df8af..ba359b47 100644 --- a/api/src/routes/users/@me/billing/payment-sources.ts +++ b/src/api/routes/users/@me/billing/payment-sources.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/users/@me/billing/subscriptions.ts b/src/api/routes/users/@me/billing/subscriptions.ts
index 411e95bf..8e407184 100644 --- a/api/src/routes/users/@me/billing/subscriptions.ts +++ b/src/api/routes/users/@me/billing/subscriptions.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/users/@me/channels.ts b/src/api/routes/users/@me/channels.ts
index 78f531e1..c17275ec 100644 --- a/api/src/routes/users/@me/channels.ts +++ b/src/api/routes/users/@me/channels.ts
@@ -1,6 +1,6 @@ -import { Request, Response, Router } from "express"; -import { Recipient, DmChannelDTO, Channel } from "@fosscord/util"; import { route } from "@fosscord/api"; +import { Channel, DmChannelCreateSchema, DmChannelDTO, Recipient } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); @@ -12,11 +12,6 @@ router.get("/", route({}), async (req: Request, res: Response) => { res.json(await Promise.all(recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id])))); }); -export interface DmChannelCreateSchema { - name?: string; - recipients: string[]; -} - router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => { const body = req.body as DmChannelCreateSchema; res.json(await Channel.createDMChannel(body.recipients, req.user_id, body.name)); diff --git a/api/src/routes/users/@me/connections.ts b/src/api/routes/users/@me/connections.ts
index 411e95bf..8e407184 100644 --- a/api/src/routes/users/@me/connections.ts +++ b/src/api/routes/users/@me/connections.ts
@@ -1,5 +1,5 @@ -import { Request, Response, Router } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts
index c24c3f1e..dfc6131b 100644 --- a/api/src/routes/users/@me/delete.ts +++ b/src/api/routes/users/@me/delete.ts
@@ -1,8 +1,14 @@ -import { Router, Request, Response } from "express"; -import { Guild, Member, User } from "@fosscord/util"; import { route } from "@fosscord/api"; -import bcrypt from "bcrypt"; -import { HTTPError } from "lambert-server"; +import { HTTPError, Member, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +let bcrypt: any; +try { + bcrypt = require("bcrypt"); +} catch { + bcrypt = require("bcryptjs"); + console.log("Warning: using bcryptjs because bcrypt is not installed! Performance will be affected."); +} const router = Router(); diff --git a/api/src/routes/users/@me/devices.ts b/src/api/routes/users/@me/devices.ts
index 8556a3ad..cb01e576 100644 --- a/api/src/routes/users/@me/devices.ts +++ b/src/api/routes/users/@me/devices.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/users/@me/disable.ts b/src/api/routes/users/@me/disable.ts
index 4aff3774..05976908 100644 --- a/api/src/routes/users/@me/disable.ts +++ b/src/api/routes/users/@me/disable.ts
@@ -1,7 +1,14 @@ -import { User } from "@fosscord/util"; -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; -import bcrypt from "bcrypt"; +import { User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +let bcrypt: any; +try { + bcrypt = require("bcrypt"); +} catch { + bcrypt = require("bcryptjs"); + console.log("Warning: using bcryptjs because bcrypt is not installed! Performance will be affected."); +} const router = Router(); diff --git a/api/src/routes/users/@me/email-settings.ts b/src/api/routes/users/@me/email-settings.ts
index 3114984e..28d0864a 100644 --- a/api/src/routes/users/@me/email-settings.ts +++ b/src/api/routes/users/@me/email-settings.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/users/@me/entitlements.ts b/src/api/routes/users/@me/entitlements.ts
index 341e2b4c..7aaa5d7c 100644 --- a/api/src/routes/users/@me/entitlements.ts +++ b/src/api/routes/users/@me/entitlements.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/users/@me/guilds.ts b/src/api/routes/users/@me/guilds.ts
index 754a240e..5141aa3d 100644 --- a/api/src/routes/users/@me/guilds.ts +++ b/src/api/routes/users/@me/guilds.ts
@@ -1,7 +1,6 @@ -import { Router, Request, Response } from "express"; -import { Guild, Member, User, GuildDeleteEvent, GuildMemberRemoveEvent, emitEvent, Config } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; +import { Config, emitEvent, Guild, GuildDeleteEvent, GuildMemberRemoveEvent, HTTPError, Member, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/routes/users/@me/guilds/premium/subscription-slots.ts b/src/api/routes/users/@me/guilds/premium/subscription-slots.ts
index 014df8af..ba359b47 100644 --- a/api/src/routes/users/@me/guilds/premium/subscription-slots.ts +++ b/src/api/routes/users/@me/guilds/premium/subscription-slots.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/api/src/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts
index 1af413c4..563300dc 100644 --- a/api/src/routes/users/@me/index.ts +++ b/src/api/routes/users/@me/index.ts
@@ -1,39 +1,40 @@ -import { Router, Request, Response } from "express"; -import { User, PrivateUserProjection, emitEvent, UserUpdateEvent, handleFile, FieldErrors } from "@fosscord/util"; import { route } from "@fosscord/api"; -import bcrypt from "bcrypt"; +import { + adjustEmail, + Config, + emitEvent, + FieldErrors, + generateToken, + handleFile, + OrmUtils, + PrivateUserProjection, + User, + UserModifySchema, + UserUpdateEvent +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; -const router: Router = Router(); - -export interface UserModifySchema { - /** - * @minLength 1 - * @maxLength 100 - */ - username?: string; - avatar?: string | null; - /** - * @maxLength 1024 - */ - bio?: string; - accent_color?: number; - banner?: string | null; - password?: string; - new_password?: string; - code?: string; +let bcrypt: any; +try { + bcrypt = require("bcrypt"); +} catch { + bcrypt = require("bcryptjs"); + console.log("Warning: using bcryptjs because bcrypt is not installed! Performance will be affected."); } +const router: Router = Router(); + router.get("/", route({}), async (req: Request, res: Response) => { res.json(await User.findOne({ select: PrivateUserProjection, where: { id: req.user_id } })); }); router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: Response) => { + var token = null as any; const body = req.body as UserModifySchema; 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 User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] }); + let user = await User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] }); if (body.password) { if (user.data?.hash) { @@ -46,6 +47,13 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: } } + if (body.email) { + body.email = adjustEmail(body.email); + if (!body.email && Config.get().register.email.required) + throw FieldErrors({ email: { message: req.t("auth:register.EMAIL_INVALID"), code: "EMAIL_INVALID" } }); + if (!body.password) throw FieldErrors({ password: { message: req.t("auth:register.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); + } + if (body.new_password) { if (!body.password && !user.email) { throw FieldErrors({ @@ -53,18 +61,20 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: }); } user.data.hash = await bcrypt.hash(body.new_password, 12); + user.data.valid_tokens_since = new Date(); + token = (await generateToken(user.id)) as string; } - if(body.username){ - var check_username = body?.username?.replace(/\s/g, ''); - if(!check_username) { - throw FieldErrors({ - username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } - }); - } - } + if (body.username) { + let check_username = body?.username?.replace(/\s/g, ""); + if (!check_username) { + throw FieldErrors({ + username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } + }); + } + } - user.assign(body); + user = OrmUtils.mergeDeep(user, body); await user.save(); // @ts-ignore @@ -77,7 +87,10 @@ router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: data: user } as UserUpdateEvent); - res.json(user); + res.json({ + ...user, + token + }); }); export default router; diff --git a/api/src/routes/users/@me/library.ts b/src/api/routes/users/@me/library.ts
index 7ac13bae..0aea02a0 100644 --- a/api/src/routes/users/@me/library.ts +++ b/src/api/routes/users/@me/library.ts
@@ -1,5 +1,5 @@ -import { Router, Response, Request } from "express"; import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts new file mode 100644
index 00000000..c62581cc --- /dev/null +++ b/src/api/routes/users/@me/mfa/codes.ts
@@ -0,0 +1,48 @@ +import { route } from "@fosscord/api"; +import { BackupCode, Config, FieldErrors, generateMfaBackupCodes, MfaCodesSchema, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +let bcrypt: any; +try { + bcrypt = require("bcrypt"); +} catch { + bcrypt = require("bcryptjs"); + console.log("Warning: using bcryptjs because bcrypt is not installed! Performance will be affected."); +} + +const router = Router(); + +// TODO: This route is replaced with users/@me/mfa/codes-verification in newer clients + +router.post("/", route({ body: "MfaCodesSchema" }), async (req: Request, res: Response) => { + const { password, regenerate } = req.body as MfaCodesSchema; + + const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); + + if (!(await bcrypt.compare(password, user.data.hash || ""))) { + throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } }); + } + + var codes: BackupCode[]; + if (regenerate && Config.get().security.twoFactor.generateBackupCodes) { + await BackupCode.update({ user: { id: req.user_id } }, { expired: true }); + + codes = generateMfaBackupCodes(req.user_id); + await Promise.all(codes.map((x) => x.save())); + } else { + codes = await BackupCode.find({ + where: { + user: { + id: req.user_id + }, + expired: false + } + }); + } + + return res.json({ + backup_codes: codes.map((x) => ({ ...x, expired: undefined })) + }); +}); + +export default router; diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts new file mode 100644
index 00000000..6bc9a5c7 --- /dev/null +++ b/src/api/routes/users/@me/mfa/totp/disable.ts
@@ -0,0 +1,40 @@ +import { route } from "@fosscord/api"; +import { BackupCode, generateToken, TotpDisableSchema, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +import { verifyToken } from "node-2fa"; + +const router = Router(); + +router.post("/", route({ body: "TotpDisableSchema" }), async (req: Request, res: Response) => { + const body = req.body as TotpDisableSchema; + + const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["totp_secret"] }); + + const backup = await BackupCode.findOne({ where: { code: body.code } }); + if (!backup) { + const ret = verifyToken(user.totp_secret!, body.code); + if (!ret || ret.delta != 0) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + } + + await User.update( + { id: req.user_id }, + { + mfa_enabled: false, + totp_secret: "" + } + ); + + await BackupCode.update( + { user: { id: req.user_id } }, + { + expired: true + } + ); + + return res.json({ + token: await generateToken(user.id) + }); +}); + +export default router; diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts new file mode 100644
index 00000000..f3a73c28 --- /dev/null +++ b/src/api/routes/users/@me/mfa/totp/enable.ts
@@ -0,0 +1,49 @@ +import { route } from "@fosscord/api"; +import { BackupCode, Config, generateMfaBackupCodes, generateToken, TotpEnableSchema, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +import { verifyToken } from "node-2fa"; + +let bcrypt: any; +try { + bcrypt = require("bcrypt"); +} catch { + bcrypt = require("bcryptjs"); + console.log("Warning: using bcryptjs because bcrypt is not installed! Performance will be affected."); +} + +const router = Router(); + +router.post("/", route({ body: "TotpEnableSchema" }), async (req: Request, res: Response) => { + const body = req.body as TotpEnableSchema; + + const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); + + // TODO: Are guests allowed to enable 2fa? + if (user.data.hash) { + if (!(await bcrypt.compare(body.password, user.data.hash))) { + throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); + } + } + + if (!body.secret) throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005); + + if (!body.code) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + + if (verifyToken(body.secret, body.code)?.delta != 0) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + + let backup_codes: BackupCode[] = []; + if (Config.get().security.twoFactor.generateBackupCodes) { + backup_codes = generateMfaBackupCodes(req.user_id); + await Promise.all(backup_codes.map((x) => x.save())); + } + + await User.update({ id: req.user_id }, { mfa_enabled: true, totp_secret: body.secret }); + + res.send({ + token: await generateToken(user.id), + backup_codes: backup_codes.map((x) => ({ ...x, expired: undefined })) + }); +}); + +export default router; diff --git a/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts new file mode 100644
index 00000000..fc207401 --- /dev/null +++ b/src/api/routes/users/@me/notes.ts
@@ -0,0 +1,53 @@ +import { route } from "@fosscord/api"; +import { emitEvent, Note, Snowflake, User } from "@fosscord/util"; +import { Request, Response, Router } from "express"; + +const router: Router = Router(); + +router.get("/:id", route({}), async (req: Request, res: Response) => { + const { id } = req.params; + + const note = await Note.findOneOrFail({ + where: { + owner: { id: req.user_id }, + target: { id: id } + } + }); + + return res.json({ + note: note?.content, + note_user_id: id, + user_id: req.user_id + }); +}); + +router.put("/:id", route({}), async (req: Request, res: Response) => { + const { id } = req.params; + const owner = await User.findOneOrFail({ where: { id: req.user_id } }); + const target = await User.findOneOrFail({ where: { id: id } }); //if noted user does not exist throw + const { note } = req.body; + + if (note && note.length) { + // upsert a note + if (await Note.findOne({ where: { owner: { id: owner.id }, target: { id: target.id } } })) { + Note.update({ owner: { id: owner.id }, target: { id: target.id } }, { owner, target, content: note }); + } else { + Note.insert({ id: Snowflake.generate(), owner, target, content: note }); + } + } else { + await Note.delete({ owner: { id: owner.id }, target: { id: target.id } }); + } + + await emitEvent({ + event: "USER_NOTE_UPDATE", + data: { + note: note, + id: target.id + }, + user_id: owner.id + }); + + return res.status(204); +}); + +export default router; diff --git a/api/src/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts
index 0c13cdba..8267c142 100644 --- a/api/src/routes/users/@me/relationships.ts +++ b/src/api/routes/users/@me/relationships.ts
@@ -1,17 +1,18 @@ +import { route } from "@fosscord/api"; import { - RelationshipAddEvent, - User, - PublicUserProjection, - RelationshipType, - RelationshipRemoveEvent, + Config, + DiscordApiErrors, emitEvent, + HTTPError, + OrmUtils, + PublicUserProjection, Relationship, - Config + RelationshipAddEvent, + RelationshipRemoveEvent, + RelationshipType, + User } from "@fosscord/util"; -import { Router, Response, Request } from "express"; -import { HTTPError } from "lambert-server"; -import { DiscordApiErrors } from "@fosscord/util"; -import { route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router = Router(); @@ -37,24 +38,19 @@ router.get("/", route({}), async (req: Request, res: Response) => { return res.json(related_users); }); -export interface RelationshipPutSchema { - type?: RelationshipType; -} - router.put("/:id", route({ body: "RelationshipPutSchema" }), async (req: Request, res: Response) => { return await updateRelationship( req, res, - await User.findOneOrFail({ id: req.params.id }, { relations: ["relationships", "relationships.to"], select: userProjection }), + await User.findOneOrFail({ + where: { id: req.params.id }, + relations: ["relationships", "relationships.to"], + select: userProjection + }), req.body.type ?? RelationshipType.friends ); }); -export interface RelationshipPostSchema { - discriminator: string; - username: string; -} - router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request, res: Response) => { return await updateRelationship( req, @@ -75,8 +71,8 @@ router.delete("/:id", route({}), 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 User.findOneOrFail({ id: req.user_id }, { select: userProjection, relations: ["relationships"] }); - const friend = await User.findOneOrFail({ id: id }, { select: userProjection, relations: ["relationships"] }); + const user = await User.findOneOrFail({ where: { id: req.user_id }, select: userProjection, relations: ["relationships"] }); + const friend = await User.findOneOrFail({ where: { id: id }, select: userProjection, relations: ["relationships"] }); const relationship = user.relationships.find((x) => x.to_id === id); const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id); @@ -124,12 +120,13 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ const id = friend.id; if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend"); - const user = await User.findOneOrFail( - { id: req.user_id }, - { relations: ["relationships", "relationships.to"], select: userProjection } - ); + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + relations: ["relationships", "relationships.to"], + select: userProjection + }); - var relationship = user.relationships.find((x) => x.to_id === id); + let relationship = user.relationships.find((x) => x.to_id === id); const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id); // TODO: you can add infinitely many blocked users (should this be prevented?) @@ -139,7 +136,9 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ relationship.type = RelationshipType.blocked; await relationship.save(); } else { - relationship = await new Relationship({ to_id: id, type: RelationshipType.blocked, from_id: req.user_id }).save(); + relationship = await ( + OrmUtils.mergeDeep(new Relationship(), { to_id: id, type: RelationshipType.blocked, from_id: req.user_id }) as Relationship + ).save(); } if (friendRequest && friendRequest.type !== RelationshipType.blocked) { @@ -165,8 +164,13 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ const { maxFriends } = Config.get().limits.user; if (user.relationships.length >= maxFriends) throw DiscordApiErrors.MAXIMUM_FRIENDS.withParams(maxFriends); - var incoming_relationship = new Relationship({ nickname: undefined, type: RelationshipType.incoming, to: user, from: friend }); - var outgoing_relationship = new Relationship({ + let incoming_relationship = OrmUtils.mergeDeep(new Relationship(), { + nickname: undefined, + type: RelationshipType.incoming, + to: user, + from: friend + }); + let outgoing_relationship = OrmUtils.mergeDeep(new Relationship(), { nickname: undefined, type: RelationshipType.outgoing, to: friend, @@ -177,7 +181,7 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you"); if (friendRequest.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user"); // accept friend request - incoming_relationship = friendRequest; + incoming_relationship = friendRequest as any; //TODO: checkme, any cast incoming_relationship.type = RelationshipType.friends; } @@ -185,7 +189,7 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ 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"); - outgoing_relationship = relationship; + outgoing_relationship = relationship as any; //TODO: checkme, any cast outgoing_relationship.type = RelationshipType.friends; } diff --git a/api/src/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts
index b22b72fb..e276a22a 100644 --- a/api/src/routes/users/@me/settings.ts +++ b/src/api/routes/users/@me/settings.ts
@@ -1,17 +1,15 @@ -import { Router, Response, Request } from "express"; -import { User, UserSettings } from "@fosscord/util"; import { route } from "@fosscord/api"; +import { User, UserSettings } from "@fosscord/util"; +import { Request, Response, Router } from "express"; const router = Router(); -export interface UserSettingsSchema extends Partial<UserSettings> {} - router.patch("/", route({ body: "UserSettingsSchema" }), async (req: Request, res: Response) => { const body = req.body as UserSettings; if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale - const user = await User.findOneOrFail({ id: req.user_id, bot: false }); - user.settings = { ...user.settings, ...body }; + const user = await User.findOneOrFail({ where: { id: req.user_id, bot: false }, relations: ["settings"] }); + user.settings = { ...user.settings, ...body } as UserSettings; await user.save(); res.sendStatus(204); diff --git a/api/src/routes/voice/regions.ts b/src/api/routes/voice/regions.ts
index 4de304ee..eacdcf11 100644 --- a/api/src/routes/voice/regions.ts +++ b/src/api/routes/voice/regions.ts
@@ -1,6 +1,5 @@ -import { Router, Request, Response } from "express"; -import { getIpAdress, route } from "@fosscord/api"; -import { getVoiceRegions } from "@fosscord/api"; +import { getIpAdress, getVoiceRegions, route } from "@fosscord/api"; +import { Request, Response, Router } from "express"; const router: Router = Router(); diff --git a/api/src/start.ts b/src/api/start.ts
index ccb4d108..c407484d 100644 --- a/api/src/start.ts +++ b/src/api/start.ts
@@ -1,17 +1,16 @@ process.on("uncaughtException", console.error); process.on("unhandledRejection", console.error); -import "missing-native-js-functions"; -import { config } from "dotenv"; -config(); -import { FosscordServer } from "./Server"; import cluster from "cluster"; +import { config } from "dotenv"; import os from "os"; -var cores = 1; +import { FosscordServer } from "./Server"; +config(); +let cores = 1; try { cores = Number(process.env.THREADS) || os.cpus().length; } catch { - console.log("[API] Failed to get thread count! Using 1...") + console.log("[API] Failed to get thread count! Using 1..."); } if (cluster.isMaster && process.env.NODE_ENV == "production") { @@ -27,7 +26,7 @@ if (cluster.isMaster && process.env.NODE_ENV == "production") { cluster.fork(); }); } else { - var port = Number(process.env.PORT) || 3001; + let port = Number(process.env.PORT) || 3001; const server = new FosscordServer({ port }); server.start().catch(console.error); diff --git a/src/api/util/entities/AssetCacheItem.ts b/src/api/util/entities/AssetCacheItem.ts new file mode 100644
index 00000000..958d5a61 --- /dev/null +++ b/src/api/util/entities/AssetCacheItem.ts
@@ -0,0 +1,3 @@ +export class AssetCacheItem { + constructor(public Key: string, public FilePath: string = "", public Headers: any = null as any) {} +} diff --git a/api/src/util/entities/blockedEmailDomains.txt b/src/api/util/entities/blockedEmailDomains.txt
index eb88305d..eb88305d 100644 --- a/api/src/util/entities/blockedEmailDomains.txt +++ b/src/api/util/entities/blockedEmailDomains.txt
diff --git a/api/src/util/entities/trustedEmailDomains.txt b/src/api/util/entities/trustedEmailDomains.txt
index 38ffa4fa..38ffa4fa 100644 --- a/api/src/util/entities/trustedEmailDomains.txt +++ b/src/api/util/entities/trustedEmailDomains.txt
diff --git a/api/src/util/handlers/Instance.ts b/src/api/util/handlers/Instance.ts
index 6bddfa98..e03c9488 100644 --- a/api/src/util/handlers/Instance.ts +++ b/src/api/util/handlers/Instance.ts
@@ -9,7 +9,7 @@ export async function initInstance() { const { autoJoin } = Config.get().guild; if (autoJoin.enabled && !autoJoin.guilds?.length) { - let guild = await Guild.findOne({}); + let guild = await Guild.findOne({ where: {}, order: { id: "ASC" } }); if (guild) { // @ts-ignore await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } }); diff --git a/api/src/util/handlers/Message.ts b/src/api/util/handlers/Message.ts
index 48f87dfe..d760d27c 100644 --- a/api/src/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts
@@ -1,31 +1,32 @@ import { + Application, + Attachment, Channel, + CHANNEL_MENTION, + Config, Embed, emitEvent, + EVERYONE_MENTION, + getPermission, + getRights, Guild, + HERE_MENTION, + HTTPError, Message, MessageCreateEvent, + MessageCreateSchema, + MessageType, MessageUpdateEvent, - getPermission, - getRights, - CHANNEL_MENTION, - Snowflake, - USER_MENTION, - ROLE_MENTION, + OrmUtils, Role, - EVERYONE_MENTION, - HERE_MENTION, - MessageType, + ROLE_MENTION, User, - Application, - Webhook, - Attachment, - Config, + USER_MENTION, + Webhook } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import fetch from "node-fetch"; import cheerio from "cheerio"; -import { MessageCreateSchema } from "../../routes/channels/#channel_id/messages"; +import fetch from "node-fetch"; + const allow_empty = false; // TODO: check webhook, application, system author, stickers // TODO: embed gifs/videos/images @@ -47,7 +48,7 @@ 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({ + const message = OrmUtils.mergeDeep(new Message(), { ...opts, sticker_items: opts.sticker_ids?.map((x) => ({ id: x })), guild_id: channel.guild_id, @@ -59,21 +60,21 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { }); if (message.content && message.content.length > Config.get().limits.message.maxCharacters) { - throw new HTTPError("Content length over max character limit") + throw new HTTPError("Content length over max character limit"); } if (opts.author_id) { message.author = await User.getPublicUser(opts.author_id); const rights = await getRights(opts.author_id); rights.hasThrow("SEND_MESSAGES"); - } + } if (opts.application_id) { - message.application = await Application.findOneOrFail({ id: opts.application_id }); + message.application = await Application.findOneOrFail({ where: { id: opts.application_id } }); } if (opts.webhook_id) { - message.webhook = await Webhook.findOneOrFail({ id: opts.webhook_id }); + message.webhook = await Webhook.findOneOrFail({ where: { id: opts.webhook_id } }); } - + const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id); permission.hasThrow("SEND_MESSAGES"); if (permission.cache.member) { @@ -85,10 +86,12 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { permission.hasThrow("READ_MESSAGE_HISTORY"); // code below has to be redone when we add custom message routing if (message.guild_id !== null) { - const guild = await Guild.findOneOrFail({ id: channel.guild_id }); + const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } }); if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) { - if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild"); - if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel"); + if (opts.message_reference.guild_id !== channel.guild_id) + throw new HTTPError("You can only reference messages from this guild"); + if (opts.message_reference.channel_id !== opts.channel_id) + throw new HTTPError("You can only reference messages from this channel"); } } /** Q: should be checked if the referenced message exists? ANSWER: NO @@ -98,17 +101,18 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { } // TODO: stickers/activity - if (!allow_empty && (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length)) { + if (!allow_empty && !opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length) { throw new HTTPError("Empty messages are not allowed", 50006); } - var content = opts.content; - var mention_channel_ids = [] as string[]; - var mention_role_ids = [] as string[]; - var mention_user_ids = [] as string[]; - var mention_everyone = false; + let content = opts.content; + let mention_channel_ids = [] as string[]; + let mention_role_ids = [] as string[]; + let mention_user_ids = [] as string[]; + let mention_everyone = false; - if (content) { // TODO: explicit-only mentions + if (content) { + // TODO: explicit-only mentions message.content = content.trim(); for (const [_, mention] of content.matchAll(CHANNEL_MENTION)) { if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(mention); @@ -120,7 +124,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { await Promise.all( Array.from(content.matchAll(ROLE_MENTION)).map(async ([_, mention]) => { - const role = await Role.findOneOrFail({ id: mention, guild_id: channel.guild_id }); + const role = await Role.findOneOrFail({ where: { id: mention, guild_id: channel.guild_id } }); if (role.mentionable || permission.has("MANAGE_ROLES")) { mention_role_ids.push(mention); } @@ -132,9 +136,9 @@ export async function handleMessage(opts: MessageOptions): Promise<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_channels = mention_channel_ids.map((x) => OrmUtils.mergeDeep(new Channel(), { id: x })); + message.mention_roles = mention_role_ids.map((x) => OrmUtils.mergeDeep(new Role(), { id: x })); + message.mentions = mention_user_ids.map((x) => OrmUtils.mergeDeep(new User(), { id: x })); message.mention_everyone = mention_everyone; // TODO: check and put it all in the body @@ -144,7 +148,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> { // TODO: cache link result in db export async function postHandleMessage(message: Message) { - var links = message.content?.match(LINK_REGEX); + let links = message.content?.match(LINK_REGEX); if (!links) return; const data = { ...message }; @@ -156,7 +160,7 @@ export async function postHandleMessage(message: Message) { try { const request = await fetch(link, { ...DEFAULT_FETCH_OPTIONS, - size: Config.get().limits.message.maxEmbedDownloadSize, + size: Config.get().limits.message.maxEmbedDownloadSize }); const text = await request.text(); @@ -201,9 +205,10 @@ export async function postHandleMessage(message: Message) { export async function sendMessage(opts: MessageOptions) { const message = await handleMessage({ ...opts, timestamp: new Date() }); + //TODO: check this, removed toJSON call await Promise.all([ Message.insert(message), - emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data: message.toJSON() } as MessageCreateEvent) + emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data: message } as MessageCreateEvent) ]); postHandleMessage(message).catch((e) => {}); // no await as it should catch error non-blockingly diff --git a/api/src/util/handlers/Voice.ts b/src/api/util/handlers/Voice.ts
index 4d60eb91..4d60eb91 100644 --- a/api/src/util/handlers/Voice.ts +++ b/src/api/util/handlers/Voice.ts
diff --git a/api/src/util/handlers/route.ts b/src/api/util/handlers/route.ts
index 3d3bbc37..d43ae103 100644 --- a/api/src/util/handlers/route.ts +++ b/src/api/util/handlers/route.ts
@@ -1,8 +1,6 @@ import { DiscordApiErrors, EVENT, - Event, - EventData, FieldErrors, FosscordApiErrors, getPermission, @@ -12,14 +10,14 @@ import { RightResolvable, Rights } from "@fosscord/util"; +import Ajv from "ajv"; +import addFormats from "ajv-formats"; +import { AnyValidateFunction } from "ajv/dist/core"; import { NextFunction, Request, Response } from "express"; import fs from "fs"; import path from "path"; -import Ajv from "ajv"; -import { AnyValidateFunction } from "ajv/dist/core"; -import addFormats from "ajv-formats"; -const SchemaPath = path.join(__dirname, "..", "..", "..", "assets", "schemas.json"); +const SchemaPath = path.join(__dirname, "..", "..", "..", "..", "assets", "schemas.json"); const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" })); export const ajv = new Ajv({ @@ -87,7 +85,7 @@ const normalizeBody = (body: any = {}) => { }; export function route(opts: RouteOptions) { - var validate: AnyValidateFunction<any> | undefined; + let validate: AnyValidateFunction<any> | undefined; if (opts.body) { validate = ajv.getSchema(opts.body); if (!validate) throw new Error(`Body schema ${opts.body} not found`); @@ -117,6 +115,11 @@ export function route(opts: RouteOptions) { const valid = validate(normalizeBody(req.body)); if (!valid) { const fields: Record<string, { code?: string; message: string }> = {}; + if (process.env.LOG_INVALID_BODY) { + console.log(`Got invalid request: ${req.method} ${req.originalUrl}`); + console.log(req.body); + validate.errors?.forEach((x) => console.log(x.params)); + } validate.errors?.forEach((x) => (fields[x.instancePath.slice(1)] = { code: x.keyword, message: x.message || "" })); throw FieldErrors(fields); } diff --git a/api/src/util/index.ts b/src/api/util/index.ts
index ffbcf24e..31e75325 100644 --- a/api/src/util/index.ts +++ b/src/api/util/index.ts
@@ -1,8 +1,10 @@ +export * from "./entities/AssetCacheItem"; +export * from "./handlers/Message"; +export * from "./handlers/route"; +export * from "./handlers/Voice"; export * from "./utility/Base64"; export * from "./utility/ipAddress"; -export * from "./handlers/Message"; export * from "./utility/passwordStrength"; export * from "./utility/RandomInviteID"; -export * from "./handlers/route"; export * from "./utility/String"; -export * from "./handlers/Voice"; +export * from "./utility/captcha"; \ No newline at end of file diff --git a/api/src/util/utility/Base64.ts b/src/api/util/utility/Base64.ts
index 46cff77a..46cff77a 100644 --- a/api/src/util/utility/Base64.ts +++ b/src/api/util/utility/Base64.ts
diff --git a/api/src/util/utility/RandomInviteID.ts b/src/api/util/utility/RandomInviteID.ts
index 7ea344e0..feebfd3d 100644 --- a/api/src/util/utility/RandomInviteID.ts +++ b/src/api/util/utility/RandomInviteID.ts
@@ -22,11 +22,10 @@ export function snowflakeBasedInvite() { // snowflakes hold ~10.75 characters worth of entropy; // safe to generate a 8-char invite out of them let str = ""; - for (let i=0; i < 10; i++) { - + for (let i = 0; i < 10; i++) { str.concat(chars.charAt(Number(snowflake % base))); snowflake = snowflake / base; } - - return str.substr(3,8).split("").reverse().join(""); + + return str.substr(3, 8).split("").reverse().join(""); } diff --git a/api/src/util/utility/String.ts b/src/api/util/utility/String.ts
index 982b7e11..a2e491e4 100644 --- a/api/src/util/utility/String.ts +++ b/src/api/util/utility/String.ts
@@ -1,6 +1,6 @@ +import { FieldErrors } from "@fosscord/util"; import { Request } from "express"; import { ntob } from "./Base64"; -import { FieldErrors } from "@fosscord/util"; export function checkLength(str: string, min: number, max: number, key: string, req: Request) { if (str.length < min || str.length > max) { diff --git a/src/api/util/utility/captcha.ts b/src/api/util/utility/captcha.ts new file mode 100644
index 00000000..739647d2 --- /dev/null +++ b/src/api/util/utility/captcha.ts
@@ -0,0 +1,46 @@ +import { Config } from "@fosscord/util"; +import fetch from "node-fetch"; + +export interface hcaptchaResponse { + success: boolean; + challenge_ts: string; + hostname: string; + credit: boolean; + "error-codes": string[]; + score: number; // enterprise only + score_reason: string[]; // enterprise only +} + +export interface recaptchaResponse { + success: boolean; + score: number; // between 0 - 1 + action: string; + challenge_ts: string; + hostname: string; + "error-codes"?: string[]; +} + +const verifyEndpoints = { + hcaptcha: "https://hcaptcha.com/siteverify", + recaptcha: "https://www.google.com/recaptcha/api/siteverify", +} + +export async function verifyCaptcha(response: string, ip?: string) { + const { security } = Config.get(); + const { service, secret, sitekey } = security.captcha; + + if (!service) throw new Error("Cannot verify captcha without service"); + + const res = await fetch(verifyEndpoints[service], { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `response=${encodeURIComponent(response)}` + + `&secret=${encodeURIComponent(secret!)}` + + `&sitekey=${encodeURIComponent(sitekey!)}` + + (ip ? `&remoteip=${encodeURIComponent(ip!)}` : ""), + }); + + return await res.json() as hcaptchaResponse | recaptchaResponse; +} \ No newline at end of file diff --git a/api/src/util/utility/ipAddress.ts b/src/api/util/utility/ipAddress.ts
index 13cc9603..c96feb9e 100644 --- a/api/src/util/utility/ipAddress.ts +++ b/src/api/util/utility/ipAddress.ts
@@ -65,7 +65,7 @@ export async function IPAnalysis(ip: string): Promise<typeof exampleData> { const { ipdataApiKey } = Config.get().security; if (!ipdataApiKey) return { ...exampleData, ip }; - return (await fetch(`https://api.ipdata.co/${ip}?api-key=${ipdataApiKey}`)).json(); + return (await fetch(`https://api.ipdata.co/${ip}?api-key=${ipdataApiKey}`)).json() as any; } export function isProxy(data: typeof exampleData) { @@ -78,7 +78,11 @@ export function isProxy(data: typeof exampleData) { export function getIpAdress(req: Request): string { // @ts-ignore - return req.headers[Config.get().security.forwadedFor] || req.socket.remoteAddress; + return ( + req.headers[Config.get().security.forwadedFor as string] || + req.headers[Config.get().security.forwadedFor?.toLowerCase() as string] || + req.socket.remoteAddress + ); } export function distanceBetweenLocations(loc1: any, loc2: any): number { diff --git a/api/src/util/utility/passwordStrength.ts b/src/api/util/utility/passwordStrength.ts
index 439700d0..ff83d3df 100644 --- a/api/src/util/utility/passwordStrength.ts +++ b/src/api/util/utility/passwordStrength.ts
@@ -1,5 +1,4 @@ import { Config } from "@fosscord/util"; -import "missing-native-js-functions"; const reNUMBER = /[0-9]/g; const reUPPERCASELETTER = /[A-Z]/g; @@ -19,7 +18,7 @@ const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored */ export function checkPassword(password: string): number { const { minLength, minNumbers, minUpperCase, minSymbols } = Config.get().register.password; - var strength = 0; + let strength = 0; // checks for total password len if (password.length >= minLength - 1) { @@ -45,16 +44,16 @@ export function checkPassword(password: string): number { if (password.length == password.count(reNUMBER) || password.length === password.count(reUPPERCASELETTER)) { strength = 0; } - + let entropyMap: { [key: string]: number } = {}; for (let i = 0; i < password.length; i++) { if (entropyMap[password[i]]) entropyMap[password[i]]++; else entropyMap[password[i]] = 1; } - + let entropies = Object.values(entropyMap); - - entropies.map(x => (x / entropyMap.length)); - strength += entropies.reduceRight((a: number, x: number) => a - (x * Math.log2(x))) / Math.log2(password.length); + + entropies.map((x) => x / entropyMap.length); + strength += entropies.reduceRight((a: number, x: number) => a - x * Math.log2(x)) / Math.log2(password.length); return strength; }