From 46c80218233967b509cdfe9e111f254bd3efdb14 Mon Sep 17 00:00:00 2001 From: Joy Date: Sat, 9 Mar 2024 16:55:17 +0530 Subject: [PATCH] Feat : Handled discord messaging failure use case --- src/constants/commands.ts | 6 +++ src/controllers/baseHandler.ts | 4 +- src/controllers/mentionEachUser.ts | 12 +++++- src/typeDefinitions/filterUsersByRole.d.ts | 5 +++ src/utils/batchDiscordRequests.ts | 4 +- src/utils/guildRole.ts | 43 +++++++++++++-------- tests/fixtures/fixture.ts | 5 +++ tests/unit/handlers/mentionEachUser.test.ts | 27 +++++++++++++ tests/unit/utils/guildRole.test.ts | 5 +-- 9 files changed, 86 insertions(+), 25 deletions(-) diff --git a/src/constants/commands.ts b/src/constants/commands.ts index a488de33..7fa44026 100644 --- a/src/constants/commands.ts +++ b/src/constants/commands.ts @@ -25,6 +25,12 @@ export const MENTION_EACH = { type: 3, require: false, }, + { + name: "dev", + description: "want to tag them individually?", + type: 5, + require: false, + }, ], }; diff --git a/src/controllers/baseHandler.ts b/src/controllers/baseHandler.ts index 835ea589..22219b80 100644 --- a/src/controllers/baseHandler.ts +++ b/src/controllers/baseHandler.ts @@ -39,6 +39,7 @@ import { REMOVED_LISTENING_MESSAGE, RETRY_COMMAND, } from "../constants/responses"; +import { DevFlag } from "../typeDefinitions/filterUsersByRole"; export async function baseHandler( message: discordMessageRequest, @@ -66,8 +67,9 @@ export async function baseHandler( // data[1] is message obj const transformedArgument = { roleToBeTaggedObj: data[0], - displayMessageObj: data[1] ?? {}, + displayMessageObj: data.find((item) => item.name === "message"), channelId: message.channel_id, + dev: data.find((item) => item.name === "dev") as unknown as DevFlag, }; return await mentionEachUser(transformedArgument, env, ctx); } diff --git a/src/controllers/mentionEachUser.ts b/src/controllers/mentionEachUser.ts index 724af258..ebe63602 100644 --- a/src/controllers/mentionEachUser.ts +++ b/src/controllers/mentionEachUser.ts @@ -6,14 +6,17 @@ import { env } from "../typeDefinitions/default.types"; import { UserArray, MentionEachUserOptions, + DevFlag, } from "../typeDefinitions/filterUsersByRole"; import { mentionEachUserInMessage } from "../utils/guildRole"; +import { checkDisplayType } from "../utils/checkDisplayType"; export async function mentionEachUser( transformedArgument: { roleToBeTaggedObj: MentionEachUserOptions; displayMessageObj?: MentionEachUserOptions; channelId: number; + dev?: DevFlag; }, env: env, ctx: ExecutionContext @@ -21,6 +24,7 @@ export async function mentionEachUser( const getMembersInServerResponse = await getMembersInServer(env); const roleId = transformedArgument.roleToBeTaggedObj.value; const msgToBeSent = transformedArgument?.displayMessageObj?.value; + const dev = transformedArgument?.dev?.value || false; // optional chaining here only because display message obj is optional argument const usersWithMatchingRole = filterUserByRoles( getMembersInServerResponse as UserArray[], @@ -32,8 +36,12 @@ export async function mentionEachUser( message: msgToBeSent, usersWithMatchingRole, }; - if (usersWithMatchingRole.length === 0) { - return discordTextResponse("Sorry no user found under this role."); + if (!dev || usersWithMatchingRole.length === 0) { + const responseData = checkDisplayType({ + usersWithMatchingRole, + msgToBeSent, + }); + return discordTextResponse(responseData); } else { ctx.waitUntil( mentionEachUserInMessage({ diff --git a/src/typeDefinitions/filterUsersByRole.d.ts b/src/typeDefinitions/filterUsersByRole.d.ts index 4d42fd2a..a54fd748 100644 --- a/src/typeDefinitions/filterUsersByRole.d.ts +++ b/src/typeDefinitions/filterUsersByRole.d.ts @@ -10,3 +10,8 @@ export type MentionEachUserOptions = { type: number; value: string; }; +export type DevFlag = { + name: string; + type: number; + value: boolean; +}; diff --git a/src/utils/batchDiscordRequests.ts b/src/utils/batchDiscordRequests.ts index bb333bc9..455c63e9 100644 --- a/src/utils/batchDiscordRequests.ts +++ b/src/utils/batchDiscordRequests.ts @@ -19,7 +19,7 @@ interface ResponseDetails { data: RequestDetails; } -const parseRateLimitRemaining = (response: Response) => { +export const parseRateLimitRemaining = (response: Response) => { let rateLimitRemaining = Number.parseInt( response.headers.get(DISCORD_HEADERS.RATE_LIMIT_REMAINING) || "0" ); @@ -27,7 +27,7 @@ const parseRateLimitRemaining = (response: Response) => { return rateLimitRemaining; }; -const parseResetAfter = (response: Response) => { +export const parseResetAfter = (response: Response) => { let resetAfter = Number.parseFloat( response.headers.get(DISCORD_HEADERS.RATE_LIMIT_RESET_AFTER) || "0" ); diff --git a/src/utils/guildRole.ts b/src/utils/guildRole.ts index 860a94b5..ef45c912 100644 --- a/src/utils/guildRole.ts +++ b/src/utils/guildRole.ts @@ -6,9 +6,13 @@ import { ROLE_REMOVED, } from "../constants/responses"; import { DISCORD_BASE_URL } from "../constants/urls"; +import { + parseRateLimitRemaining, + parseResetAfter, +} from "../utils/batchDiscordRequests"; + import { env } from "../typeDefinitions/default.types"; import { - DiscordMessageResponse, createNewRole, discordMessageError, discordMessageRequest, @@ -150,9 +154,10 @@ export async function mentionEachUserInMessage({ channelId: number; env: env; }) { - const batchSize = 10; - let failedAPICalls = 0; + const batchSize = 5; + let waitTillNextAPICall = 0; try { + const failedUsers: Array = []; for (let i = 0; i < userIds.length; i += batchSize) { const batchwiseUserIds = userIds.slice(i, i + batchSize); const messageRequest = batchwiseUserIds.map((userId) => { @@ -165,24 +170,30 @@ export async function mentionEachUserInMessage({ body: JSON.stringify({ content: `${message ? message + " " : ""} ${userId}`, }), - }).then((response) => response.json()) as Promise< - discordMessageRequest | discordMessageError - >; + }).then((response) => { + const rateLimitRemaining = parseRateLimitRemaining(response); + if (rateLimitRemaining === 0) { + waitTillNextAPICall = Math.max( + parseResetAfter(response), + waitTillNextAPICall + ); + } + return response.json(); + }) as Promise; }); const responses = await Promise.all(messageRequest); - responses.forEach((response) => { - if ( - response && - "message" in response && - response.message === "404: Not Found" - ) { - failedAPICalls += 1; + responses.forEach((response, i) => { + if (response && "message" in response) { + failedUsers.push(batchwiseUserIds[i]); console.error(`Failed to mention a user`); } }); - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => + setTimeout(resolve, waitTillNextAPICall * 1000) + ); + waitTillNextAPICall = 0; } - if (failedAPICalls > 0) { + if (failedUsers.length > 0) { await fetch(`${DISCORD_BASE_URL}/channels/${channelId}/messages`, { method: "POST", headers: { @@ -190,7 +201,7 @@ export async function mentionEachUserInMessage({ Authorization: `Bot ${env.DISCORD_TOKEN}`, }, body: JSON.stringify({ - content: `Failed to tag ${failedAPICalls} users`, + content: `Failed to tag ${failedUsers} individually.`, }), }); } diff --git a/tests/fixtures/fixture.ts b/tests/fixtures/fixture.ts index 668cfe71..a6aadcbd 100644 --- a/tests/fixtures/fixture.ts +++ b/tests/fixtures/fixture.ts @@ -107,6 +107,11 @@ export const onlyRoleToBeTagged = { value: "1118201414078976192", }, channelId: 1244, + dev: { + name: "dev", + type: 4, + value: false, + }, }; export const ctx = { diff --git a/tests/unit/handlers/mentionEachUser.test.ts b/tests/unit/handlers/mentionEachUser.test.ts index 6ad03eb9..dc23bc22 100644 --- a/tests/unit/handlers/mentionEachUser.test.ts +++ b/tests/unit/handlers/mentionEachUser.test.ts @@ -19,6 +19,33 @@ describe("Test mention each function", () => { expect(response).toBeInstanceOf(Promise); }); + it("should run without displayMessageObj argument in dev mode", async () => { + const env = { + BOT_PUBLIC_KEY: "xyz", + DISCORD_GUILD_ID: "123", + DISCORD_TOKEN: "abc", + }; + const response = mentionEachUser( + { + ...onlyRoleToBeTagged, + dev: { + name: "dev", + type: 4, + value: true, + }, + }, + env, + ctx + ); + expect(response).toBeInstanceOf(Promise); + const textMessage: { data: { content: string } } = await response.then( + (res) => res.json() + ); + expect(textMessage.data.content).toBe( + "Sorry no user found under this role." + ); + }); + it("should run without displayMessageObj argument", async () => { const env = { BOT_PUBLIC_KEY: "xyz", diff --git a/tests/unit/utils/guildRole.test.ts b/tests/unit/utils/guildRole.test.ts index 0ee8fca7..047a2c1a 100644 --- a/tests/unit/utils/guildRole.test.ts +++ b/tests/unit/utils/guildRole.test.ts @@ -15,10 +15,7 @@ import { mockMessageResponse, rolesMock, } from "../../fixtures/fixture"; -import { - DiscordMessageResponse, - discordMessageRequest, -} from "../../../src/typeDefinitions/discordMessage.types"; +import { DiscordMessageResponse } from "../../../src/typeDefinitions/discordMessage.types"; describe("createGuildRole", () => { it("should pass the reason to discord as a X-Audit-Log-Reason header if provided", async () => {