From 2e84840213afe7682af56beedfd817ed6854248c Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Sun, 24 Apr 2022 16:09:38 +1000 Subject: While backfilling, message ids must now be valid snowflakes, cannot be in the future, and cannot overwrite existing messages --- .../#channel_id/messages/#message_id/index.ts | 24 +++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts index 6d2bf185..8d2bd5cb 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts @@ -16,6 +16,8 @@ import multer from "multer"; import { route } from "@fosscord/api"; import { handleMessage, postHandleMessage } from "@fosscord/api"; import { MessageCreateSchema } from "../index"; +import { Snowflake } from "@fosscord/util"; +import { HTTPError } from "lambert-server"; const router = Router(); // TODO: message content/embed string length limit @@ -91,6 +93,22 @@ router.put( var body = req.body as MessageCreateSchema; const attachments: Attachment[] = []; + // regex to check if message contains anything other than numerals ( also no decimals ) + if (!message_id.match(/^\+?\d+$/)) { + throw new HTTPError("Message IDs must be positive integers") + } + + const snowflake = Snowflake.deconstruct(message_id) + if (Date.now() < snowflake.timestamp) { + // message is in the future + throw new HTTPError("You cannot backfill messages in the future", 400); + } + + const exists = await Message.findOne({ where: { id: message_id, channel_id: channel_id }}); + if (exists) { + throw new HTTPError("Cannot backfill to message ID that already exists", 400); + } + if (req.file) { try { const file = await uploadFile(`/attachments/${req.params.channel_id}`, req.file); @@ -100,8 +118,6 @@ router.put( } } const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }); - - // TODO: check the ID is not from the future, to prevent future-faking of channel histories const embeds = body.embeds || []; if (body.embed) embeds.push(body.embed); @@ -115,11 +131,9 @@ router.put( channel_id, attachments, edited_timestamp: undefined, - timestamp: undefined, // FIXME: calculate timestamp from snowflake + timestamp: new Date(snowflake.timestamp), }); - channel.last_message_id = message.id; - //Fix for the client bug delete message.member -- cgit 1.5.1 From 122abacf17d8508c471f7139b0607d297d1f6357 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 09:23:52 +0300 Subject: Backfilling privilege does not imply right to post messages --- api/src/routes/channels/#channel_id/messages/#message_id/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts index 8d2bd5cb..cf25f916 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts @@ -92,10 +92,13 @@ router.put( const { channel_id, message_id } = req.params; var body = req.body as MessageCreateSchema; const attachments: Attachment[] = []; + + const rights = getRights(req.user_id); + rights.hasThrow("SEND_MESSAGES"); // regex to check if message contains anything other than numerals ( also no decimals ) if (!message_id.match(/^\+?\d+$/)) { - throw new HTTPError("Message IDs must be positive integers") + throw new HTTPError("Message IDs must be positive integers", 400); } const snowflake = Snowflake.deconstruct(message_id) @@ -106,7 +109,7 @@ router.put( const exists = await Message.findOne({ where: { id: message_id, channel_id: channel_id }}); if (exists) { - throw new HTTPError("Cannot backfill to message ID that already exists", 400); + throw new HTTPError("Cannot backfill to message ID that already exists", 409); } if (req.file) { -- cgit 1.5.1 From 6a1e2edd8ddc7e5aef8ed2847431673eb5b0084b Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 11:40:24 +0300 Subject: use return codes to allow for automation --- api/src/routes/channels/#channel_id/messages/#message_id/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts index cf25f916..958954b6 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts @@ -2,13 +2,16 @@ import { Attachment, Channel, Embed, + DiscordApiErrors, emitEvent, + FosscordApiErrors, getPermission, getRights, Message, MessageCreateEvent, MessageDeleteEvent, MessageUpdateEvent, + Snowflake, uploadFile } from "@fosscord/util"; import { Router, Response, Request } from "express"; @@ -16,7 +19,6 @@ import multer from "multer"; import { route } from "@fosscord/api"; import { handleMessage, postHandleMessage } from "@fosscord/api"; import { MessageCreateSchema } from "../index"; -import { Snowflake } from "@fosscord/util"; import { HTTPError } from "lambert-server"; const router = Router(); @@ -104,12 +106,12 @@ router.put( const snowflake = Snowflake.deconstruct(message_id) if (Date.now() < snowflake.timestamp) { // message is in the future - throw new HTTPError("You cannot backfill messages in the future", 400); + throw FosscordApiErrors.CANNOT_BACKFILL_TO_THE_FUTURE; } const exists = await Message.findOne({ where: { id: message_id, channel_id: channel_id }}); if (exists) { - throw new HTTPError("Cannot backfill to message ID that already exists", 409); + throw FosscordApiErrors.CANNOT_REPLACE_BY_BACKFILL; } if (req.file) { -- cgit 1.5.1 From 037bd43d16b11c85d3a521c7ad364b0ffe7c231a Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 14:57:26 +0300 Subject: Punitive rate limiting --- api/src/middlewares/RateLimit.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'api/src') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 1a38cfcf..8368d14a 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -53,12 +53,12 @@ export default function rateLimit(opts: { if (opts.GET && ["GET", "OPTIONS", "HEAD"].includes(req.method)) max_hits = opts.GET; else if (opts.MODIFY && ["POST", "DELETE", "PATCH", "PUT"].includes(req.method)) max_hits = opts.MODIFY; - const offender = Cache.get(executor_id + bucket_id); + let offender = Cache.get(executor_id + bucket_id); if (offender) { - const reset = offender.expires_at.getTime(); - const resetAfterMs = reset - Date.now(); - const resetAfterSec = resetAfterMs / 1000; + let reset = offender.expires_at.getTime(); + let resetAfterMs = reset - Date.now(); + let resetAfterSec = (resetAfterMs + 999) / 1000; if (resetAfterMs <= 0) { offender.hits = 0; @@ -70,6 +70,10 @@ export default function rateLimit(opts: { if (offender.blocked) { const global = bucket_id === "global"; + reset = reset + opts.window * 1000; // each block violation pushes the expiry one full window further + offender.expires_at += opts.window * 1000; + resetAfterMs = reset - Date.now(); + resetAfterSec = (resetAfterMs + 999) / 1000; console.log("blocked bucket: " + bucket_id, { resetAfterMs }); return ( -- cgit 1.5.1 From 8769bb2868d1a48a7da34fc0d7f6152a51ea37c3 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 17:35:09 +0300 Subject: fix the seconds rounding logic --- api/src/middlewares/RateLimit.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'api/src') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 8368d14a..b4f32131 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -58,7 +58,7 @@ export default function rateLimit(opts: { if (offender) { let reset = offender.expires_at.getTime(); let resetAfterMs = reset - Date.now(); - let resetAfterSec = (resetAfterMs + 999) / 1000; + let resetAfterSec = Math.ceil(resetAfterMs / 1000); if (resetAfterMs <= 0) { offender.hits = 0; @@ -73,7 +73,7 @@ export default function rateLimit(opts: { reset = reset + opts.window * 1000; // each block violation pushes the expiry one full window further offender.expires_at += opts.window * 1000; resetAfterMs = reset - Date.now(); - resetAfterSec = (resetAfterMs + 999) / 1000; + resetAfterSec = Math.ceil(resetAfterMs / 1000); console.log("blocked bucket: " + bucket_id, { resetAfterMs }); return ( -- cgit 1.5.1 From ea1f188ccec325e2128d155fe6595e431b6aea5d Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 21:49:04 +0300 Subject: Update RateLimit.ts --- api/src/middlewares/RateLimit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'api/src') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index b4f32131..f31aa5da 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -71,7 +71,7 @@ export default function rateLimit(opts: { if (offender.blocked) { const global = bucket_id === "global"; reset = reset + opts.window * 1000; // each block violation pushes the expiry one full window further - offender.expires_at += opts.window * 1000; + offender.expires_at = offender.expires_at + opts.window * 1000; resetAfterMs = reset - Date.now(); resetAfterSec = Math.ceil(resetAfterMs / 1000); -- cgit 1.5.1 From aacf99d82a2b2dce609e7426396743c8cbcb7318 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 23:04:55 +0300 Subject: Update RateLimit.ts --- api/src/middlewares/RateLimit.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'api/src') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index f31aa5da..4bbef520 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -70,8 +70,9 @@ export default function rateLimit(opts: { if (offender.blocked) { const global = bucket_id === "global"; - reset = reset + opts.window * 1000; // each block violation pushes the expiry one full window further - offender.expires_at = offender.expires_at + opts.window * 1000; + // each block violation pushes the expiry one full window further + reset = new Date(reset.getTime() + opts.window * 1000); + offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000); resetAfterMs = reset - Date.now(); resetAfterSec = Math.ceil(resetAfterMs / 1000); -- cgit 1.5.1 From 9bd5fce30c83aacb4175b4e301529592d4dcdd75 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 24 Apr 2022 23:07:25 +0300 Subject: eventually fix those errors --- api/src/middlewares/RateLimit.ts | 2 +- util/src/util/Constants.ts | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'api/src') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 4bbef520..81668034 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -71,7 +71,7 @@ export default function rateLimit(opts: { if (offender.blocked) { const global = bucket_id === "global"; // each block violation pushes the expiry one full window further - reset = new Date(reset.getTime() + opts.window * 1000); + reset += opts.window * 1000; offender.expires_at = new Date(offender.expires_at.getTime() + opts.window * 1000); resetAfterMs = reset - Date.now(); resetAfterSec = Math.ceil(resetAfterMs / 1000); diff --git a/util/src/util/Constants.ts b/util/src/util/Constants.ts index 8d61b9b4..a5d3fcd2 100644 --- a/util/src/util/Constants.ts +++ b/util/src/util/Constants.ts @@ -727,21 +727,23 @@ export const DiscordApiErrors = { * An error encountered while performing an API request (Fosscord only). Here are the potential errors: */ export const FosscordApiErrors = { - MANUALLY_TRIGGERED_ERROR: new ApiError("This is an artificial error", 1), + MANUALLY_TRIGGERED_ERROR: new ApiError("This is an artificial error", 1, 500), PREMIUM_DISABLED_FOR_GUILD: new ApiError("This guild cannot be boosted", 25001), NO_FURTHER_PREMIUM: new ApiError("This guild does not receive further boosts", 25002), - GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError("This guild cannot be boosted by you", 25003), + GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError("This guild cannot be boosted by you", 25003, 403), CANNOT_FRIEND_SELF: new ApiError("Cannot friend oneself", 25009), USER_SPECIFIC_INVITE_WRONG_RECIPIENT: new ApiError("This invite is not meant for you", 25010), USER_SPECIFIC_INVITE_FAILED: new ApiError("Failed to invite user", 25011), - CANNOT_MODIFY_USER_GROUP: new ApiError("This user cannot manipulate this group", 25050), + CANNOT_MODIFY_USER_GROUP: new ApiError("This user cannot manipulate this group", 25050, 403), CANNOT_REMOVE_SELF_FROM_GROUP: new ApiError("This user cannot remove oneself from user group", 25051), CANNOT_BAN_OPERATOR: new ApiError("Non-OPERATOR cannot ban OPERATOR from instance", 25052), - CANNOT_LEAVE_GUILD: new ApiError("You are not allowed to leave guilds that you joined by yourself", 25059), - EDITS_DISABLED: new ApiError("You are not allowed to edit your own messages", 25060), - DELETE_MESSAGE_DISABLED: new ApiError("You are not allowed to delete your own messages", 25061), - FEATURE_PERMANENTLY_DISABLED: new ApiError("This feature has been disabled server-side", 45006), + CANNOT_LEAVE_GUILD: new ApiError("You are not allowed to leave guilds that you joined by yourself", 25059, 403), + EDITS_DISABLED: new ApiError("You are not allowed to edit your own messages", 25060, 403), + DELETE_MESSAGE_DISABLED: new ApiError("You are not allowed to delete your own messages", 25061, 403), + FEATURE_PERMANENTLY_DISABLED: new ApiError("This feature has been disabled server-side", 45006, 501), MISSING_RIGHTS: new ApiError("You lack rights to perform that action ({})", 50013, undefined, [""]), + CANNOT_REPLACE_BY_BACKFILL: new ApiError("Cannot backfill to message ID that already exists", 55002, 409), + CANNOT_BACKFILL_TO_THE_FUTURE: new ApiError("You cannot backfill messages in the future", 55003), CANNOT_GRANT_PERMISSIONS_EXCEEDING_RIGHTS: new ApiError("You cannot grant permissions exceeding your own rights", 50050), ROUTES_LOOPING: new ApiError("Loops in the route definition ({})", 50060, undefined, [""]), CANNOT_REMOVE_ROUTE: new ApiError("Cannot remove message route while it is in effect and being used", 50061), @@ -787,3 +789,4 @@ function keyMirror(arr: string[]) { for (const value of arr) tmp[value] = value; return tmp; } + -- cgit 1.5.1 From 2e451d8fd0d85394cc219d7675f3537b8cc9ee90 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Thu, 28 Apr 2022 21:30:41 +0300 Subject: exempt users logic resolves #396 --- api/src/middlewares/RateLimit.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'api/src') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 81668034..7d5c51e2 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -1,4 +1,4 @@ -import { Config, listenEvent } from "@fosscord/util"; +import { Config, getRights, listenEvent, Rights } from "@fosscord/util"; import { NextFunction, Request, Response, Router } from "express"; import { getIpAdress } from "@fosscord/api"; import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; @@ -9,6 +9,7 @@ import { API_PREFIX_TRAILING_SLASH } from "./Authentication"; /* ? bucket limit? Max actions/sec per bucket? +(ANSWER: a small fosscord instance might not need a complex rate limiting system) TODO: delay database requests to include multiple queries TODO: different for methods (GET/POST) @@ -44,9 +45,12 @@ export default function rateLimit(opts: { onlyIp?: boolean; }): any { return async (req: Request, res: Response, next: NextFunction): Promise => { + // exempt user? if so, immediately short circuit + if (getRights(req.user_id).has("BYPASS_RATE_LIMITS")) return; + const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); var executor_id = getIpAdress(req); - if (!opts.onlyIp && req.user_id) executor_id = req.user_id; + if (!opts.onlyIp && req.user_id) executor_id = req.user_id; var max_hits = opts.count; if (opts.bot && req.user_bot) max_hits = opts.bot; -- cgit 1.5.1 From f09daaa3de902641ca9772089b8af676e05543c3 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Thu, 28 Apr 2022 21:38:39 +0300 Subject: needs to be async --- api/src/middlewares/RateLimit.ts | 3 ++- api/src/routes/channels/#channel_id/messages/#message_id/index.ts | 2 +- bundle/package-lock.json | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) (limited to 'api/src') diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index 7d5c51e2..ca6de98f 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -46,7 +46,8 @@ export default function rateLimit(opts: { }): any { return async (req: Request, res: Response, next: NextFunction): Promise => { // exempt user? if so, immediately short circuit - if (getRights(req.user_id).has("BYPASS_RATE_LIMITS")) return; + const rights = await getRights(req.user_id); + if (rights.has("BYPASS_RATE_LIMITS")) return; const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); var executor_id = getIpAdress(req); diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts index 958954b6..63fee9b9 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts @@ -95,7 +95,7 @@ router.put( var body = req.body as MessageCreateSchema; const attachments: Attachment[] = []; - const rights = getRights(req.user_id); + const rights = await getRights(req.user_id); rights.hasThrow("SEND_MESSAGES"); // regex to check if message contains anything other than numerals ( also no decimals ) diff --git a/bundle/package-lock.json b/bundle/package-lock.json index 4742b4a4..6fbd6978 100644 --- a/bundle/package-lock.json +++ b/bundle/package-lock.json @@ -101,7 +101,7 @@ "name": "@fosscord/api", "version": "1.0.0", "hasInstallScript": true, - "license": "GPLV3", + "license": "AGPLV3", "dependencies": { "@babel/preset-env": "^7.15.8", "@babel/preset-typescript": "^7.15.0", @@ -164,7 +164,7 @@ "../cdn": { "name": "@fosscord/cdn", "version": "1.0.0", - "license": "GPLV3", + "license": "AGPLV3", "dependencies": { "@aws-sdk/client-s3": "^3.36.1", "@aws-sdk/node-http-handler": "^3.36.0", @@ -208,7 +208,7 @@ "name": "@fosscord/gateway", "version": "1.0.0", "hasInstallScript": true, - "license": "GPLV3", + "license": "AGPLV3", "dependencies": { "@fosscord/util": "file:../util", "amqplib": "^0.8.0", -- cgit 1.5.1 From 3343df5c235382910bf3e0e5ebd06e754bc00157 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Thu, 28 Apr 2022 21:51:35 +0300 Subject: Update prune.ts --- api/src/routes/guilds/#guild_id/prune.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/guilds/#guild_id/prune.ts b/api/src/routes/guilds/#guild_id/prune.ts index 0dd4d610..0e587d22 100644 --- a/api/src/routes/guilds/#guild_id/prune.ts +++ b/api/src/routes/guilds/#guild_id/prune.ts @@ -11,6 +11,10 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n //Snowflake should have `generateFromTime` method? Or similar? var 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({ where: [ { @@ -47,7 +51,7 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n return members; }; -router.get("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const days = parseInt(req.query.days as string); var roles = req.query.include_roles; @@ -65,7 +69,7 @@ export interface PruneSchema { days: number; } -router.post("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { +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; -- cgit 1.5.1 From cec8cea01a1fd189464061785c1b6cb0dcff740b Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Thu, 28 Apr 2022 22:31:21 +0300 Subject: introduce the purge endpoint closes #281 --- .../routes/channels/#channel_id/messages/index.ts | 4 +- api/src/routes/channels/#channel_id/purge.ts | 75 ++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 api/src/routes/channels/#channel_id/purge.ts (limited to 'api/src') diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts index 34cc5ff8..a77b459b 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts @@ -30,6 +30,8 @@ export function isTextChannel(type: ChannelType): boolean { case ChannelType.GUILD_VOICE: case ChannelType.GUILD_STAGE_VOICE: case ChannelType.GUILD_CATEGORY: + case ChannelType.GUILD_FORUM: + case ChannelType.DIRECTORY: throw new HTTPError("not a text channel", 400); case ChannelType.DM: case ChannelType.GROUP_DM: @@ -155,7 +157,7 @@ const messageUpload = multer({ // https://discord.com/developers/docs/resources/channel#create-message // TODO: text channel slowdown // TODO: trim and replace message content and every embed field -// TODO: check allowed_mentions +// TODO: only dispatch mentions denoted in allowed_mentions // Send message router.post( diff --git a/api/src/routes/channels/#channel_id/purge.ts b/api/src/routes/channels/#channel_id/purge.ts new file mode 100644 index 00000000..e66034e4 --- /dev/null +++ b/api/src/routes/channels/#channel_id/purge.ts @@ -0,0 +1,75 @@ +import { HTTPError } from "lambert-server"; +import { route } from "@fosscord/api"; +import { isTextChannel } from "./messages"; +import { FindManyOptions, Between } from "typeorm"; +import { + Attachment, + Channel, + Config, + Embed, + DiscordApiErrors, + emitEvent, + FosscordApiErrors, + getPermission, + getRights, + Message, + MessageDeleteBulkEvent, + Snowflake, + uploadFile +} from "@fosscord/util"; +import { Router, Response, Request } from "express"; +import multer from "multer"; +import { handleMessage, postHandleMessage } from "@fosscord/api"; + +const router: Router = Router(); + +export default router; + +export interface PurgeSchema { + before: string; + after: string +} + +// TODO: should users be able to bulk delete messages or only bots? +// TODO: should this request fail, if you provide messages older than 14 days/invalid ids? +// https://discord.com/developers/docs/resources/channel#bulk-delete-messages +router.post("/", route({ body: "PurgeSchema", right: "SELF_DELETE_MESSAGES" }), async (req: Request, res: Response) => { + const { channel_id } = req.params; + const channel = await Channel.findOneOrFail({ 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 + var query: FindManyOptions & { where: { id?: any; }; } = { + order: { id: "ASC" }, + // take: limit, + where: { + channel_id, + id: Between(after, before), // the right way around + }, + relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] + }; + + const messages = await Message.find(query); + const endpoint = Config.get().cdn.endpointPublic; + + await Message.delete(messages.map((x) => ({ id: x }))); + + 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); +}); -- cgit 1.5.1 From 6950166ee86673ddc4880c588c15dee6a2f0e9a3 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sat, 30 Apr 2022 00:39:44 +0300 Subject: patch for missing router schema, and also add purge route self-deletion checks --- api/src/routes/channels/#channel_id/purge.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/channels/#channel_id/purge.ts b/api/src/routes/channels/#channel_id/purge.ts index e66034e4..8a87c379 100644 --- a/api/src/routes/channels/#channel_id/purge.ts +++ b/api/src/routes/channels/#channel_id/purge.ts @@ -1,7 +1,7 @@ import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; import { isTextChannel } from "./messages"; -import { FindManyOptions, Between } from "typeorm"; +import { FindManyOptions, Between, Not } from "typeorm"; import { Attachment, Channel, @@ -33,7 +33,7 @@ export interface PurgeSchema { // TODO: should users be able to bulk delete messages or only bots? // TODO: should this request fail, if you provide messages older than 14 days/invalid ids? // https://discord.com/developers/docs/resources/channel#bulk-delete-messages -router.post("/", route({ body: "PurgeSchema", right: "SELF_DELETE_MESSAGES" }), async (req: Request, res: Response) => { +router.post("/", route({ /*body: "PurgeSchema",*/ }), async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); @@ -50,18 +50,27 @@ router.post("/", route({ body: "PurgeSchema", right: "SELF_DELETE_MESSAGES" }), const { before, after } = req.body as PurgeSchema; // TODO: send the deletion event bite-by-bite to prevent client stress + var query: FindManyOptions & { 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(messages.map((x) => ({ id: x }))); -- cgit 1.5.1 From 9711f9507224906c42ce167f7cc9f4c057777fb1 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sat, 30 Apr 2022 07:44:44 +0300 Subject: messages before/after soundness check --- api/src/routes/channels/#channel_id/messages/index.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts index a77b459b..9a56106a 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts @@ -11,6 +11,7 @@ import { getRights, Message, MessageCreateEvent, + Snowflake, uploadFile, Member } from "@fosscord/util"; @@ -86,7 +87,7 @@ router.get("/", async (req: Request, res: Response) => { const before = req.query.before ? `${req.query.before}` : undefined; const after = req.query.after ? `${req.query.after}` : undefined; const limit = Number(req.query.limit) || 50; - if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100"); + if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100", 422); var halfLimit = Math.floor(limit / 2); @@ -100,9 +101,16 @@ router.get("/", async (req: Request, res: Response) => { where: { channel_id }, relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] }; + - if (after) query.where.id = MoreThan(after); - else if (before) query.where.id = LessThan(before); + if (after) { + if (after > new Snowflake()) return res.status(422); + query.where.id = MoreThan(after); + } + else if (before) { + if (before < req.params.channel_id) return res.status(422); + query.where.id = LessThan(before); + } else if (around) { query.where.id = [ MoreThan((BigInt(around) - BigInt(halfLimit)).toString()), @@ -243,3 +251,4 @@ router.post( return res.json(message); } ); + -- cgit 1.5.1 From 0101e62075d5c24207598f74734706475394c8b0 Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 15 May 2022 00:16:50 +0300 Subject: Update purge.ts --- api/src/routes/channels/#channel_id/purge.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/channels/#channel_id/purge.ts b/api/src/routes/channels/#channel_id/purge.ts index 8a87c379..28b52b50 100644 --- a/api/src/routes/channels/#channel_id/purge.ts +++ b/api/src/routes/channels/#channel_id/purge.ts @@ -30,9 +30,9 @@ export interface PurgeSchema { after: string } -// TODO: should users be able to bulk delete messages or only bots? -// TODO: should this request fail, if you provide messages older than 14 days/invalid ids? -// https://discord.com/developers/docs/resources/channel#bulk-delete-messages +/** +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({ id: channel_id }); -- cgit 1.5.1 From c5709acb9ee2400668c5abdb462153b9ddf22cdd Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 15 May 2022 00:23:17 +0300 Subject: Update bulk-delete.ts --- .../channels/#channel_id/messages/bulk-delete.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts index 7a711cb0..6eacf249 100644 --- a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts @@ -1,5 +1,5 @@ import { Router, Response, Request } from "express"; -import { Channel, Config, emitEvent, getPermission, MessageDeleteBulkEvent, Message } from "@fosscord/util"; +import { Channel, Config, emitEvent, getPermission, getRights, MessageDeleteBulkEvent, Message } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { route } from "@fosscord/api"; import { In } from "typeorm"; @@ -12,22 +12,28 @@ export interface BulkDeleteSchema { messages: string[]; } -// TODO: should users be able to bulk delete messages or only bots? -// TODO: should this request fail, if you provide messages older than 14 days/invalid ids? +// 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 }); 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); - permission.hasThrow("MANAGE_MESSAGES"); - + const { maxBulkDelete } = Config.get().limits.message; const { messages } = req.body as { messages: string[] }; - if (messages.length < 2) throw new HTTPError("You must at least specify 2 messages to bulk delete"); - if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`); + if (messages.length === 0) throw new HTTPError("You must specify messages to bulk delete"); + if (!superuser) { + permission.hasThrow("MANAGE_MESSAGES"); + if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`); + } await Message.delete(messages.map((x) => ({ id: x }))); -- cgit 1.5.1 From 1653f2c50594fbb900fe4900d5f26174d16c14fc Mon Sep 17 00:00:00 2001 From: Erkin Alp Güney Date: Sun, 15 May 2022 17:43:30 +0300 Subject: Update index.ts --- .../routes/channels/#channel_id/messages/index.ts | 32 ++++++++++++---------- 1 file changed, 18 insertions(+), 14 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts index 9a56106a..2d6a2977 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts @@ -71,7 +71,11 @@ export interface MessageCreateSchema { }; payload_json?: string; file?: any; - attachments?: any[]; //TODO we should create an interface for attachments + /** + TODO: we should create an interface for attachments + TODO: OpenWAAO<-->attachment-style metadata conversion + **/ + attachments?: any[]; sticker_ids?: string[]; } @@ -136,9 +140,12 @@ 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`. + + /** + 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]; @@ -158,15 +165,14 @@ const messageUpload = multer({ }, storage: multer.memoryStorage() }); // max upload 50 mb +/** + TODO: dynamically change limit of MessageCreateSchema with config -// TODO: dynamically change limit of MessageCreateSchema with config -// TODO: check: sum of all characters in an embed structure must not exceed instance limits - -// https://discord.com/developers/docs/resources/channel#create-message -// TODO: text channel slowdown -// TODO: trim and replace message content and every embed field -// TODO: only dispatch mentions denoted in allowed_mentions - + https://discord.com/developers/docs/resources/channel#create-message + TODO: text channel slowdown (per-user and across-users) + Q: trim and replace message content and every embed field A: NO, given this cannot be implemented in E2EE channels + TODO: only dispatch notifications for mentions denoted in allowed_mentions +**/ // Send message router.post( "/", @@ -233,8 +239,6 @@ router.post( }) ); } - - //Fix for the client bug delete message.member -- cgit 1.5.1 From f420cfd36c8ed263487c9aac2209a3052eecc3a7 Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Mon, 30 May 2022 23:28:23 +1000 Subject: Respect register_dateOfBirth_required = false --- api/src/routes/auth/register.ts | 2 +- util/src/entities/Config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'api/src') diff --git a/api/src/routes/auth/register.ts b/api/src/routes/auth/register.ts index cd1bcb72..94dd6502 100644 --- a/api/src/routes/auth/register.ts +++ b/api/src/routes/auth/register.ts @@ -128,7 +128,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re throw FieldErrors({ date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } }); - } else if (register.dateOfBirth.minimum) { + } else if (register.dateOfBirth.required && register.dateOfBirth.minimum) { const minimum = new Date(); minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); body.date_of_birth = new Date(body.date_of_birth as Date); diff --git a/util/src/entities/Config.ts b/util/src/entities/Config.ts index 8d29b387..063a4d4d 100644 --- a/util/src/entities/Config.ts +++ b/util/src/entities/Config.ts @@ -324,7 +324,7 @@ export const DefaultConfigOptions: ConfigValue = { // domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"), }, dateOfBirth: { - required: false, + required: true, minimum: 13, }, disabled: false, -- cgit 1.5.1