Skip to content

Commit

Permalink
Feat : Updated mention each flow (#205)
Browse files Browse the repository at this point in the history
* Feat : Updated mention each flow

* Feat : Handled discord messaging failure use case

* Refactor : Moved to delay functionality to util
  • Loading branch information
joyguptaa authored Mar 19, 2024
1 parent 1db2a5e commit bc327ba
Show file tree
Hide file tree
Showing 13 changed files with 352 additions and 17 deletions.
6 changes: 6 additions & 0 deletions src/constants/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export const MENTION_EACH = {
type: 3,
require: false,
},
{
name: "dev",
description: "want to tag them individually?",
type: 5,
require: false,
},
],
};

Expand Down
10 changes: 7 additions & 3 deletions src/controllers/baseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ import {
REMOVED_LISTENING_MESSAGE,
RETRY_COMMAND,
} from "../constants/responses";
import { DevFlag } from "../typeDefinitions/filterUsersByRole";

export async function baseHandler(
message: discordMessageRequest,
env: env
env: env,
ctx: ExecutionContext
): Promise<JSONResponse> {
const command = lowerCaseMessageCommands(message);

Expand All @@ -65,9 +67,11 @@ 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);
return await mentionEachUser(transformedArgument, env, ctx);
}

case getCommandName(LISTENING): {
Expand Down
36 changes: 31 additions & 5 deletions src/controllers/mentionEachUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,53 @@ 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
env: env,
ctx: ExecutionContext
) {
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[],
roleId
);
const responseData = checkDisplayType({
const payload = {
channelId: transformedArgument.channelId,
roleId: roleId,
message: msgToBeSent,
usersWithMatchingRole,
msgToBeSent,
});
return discordTextResponse(responseData);
};
if (!dev || usersWithMatchingRole.length === 0) {
const responseData = checkDisplayType({
usersWithMatchingRole,
msgToBeSent,
});
return discordTextResponse(responseData);
} else {
ctx.waitUntil(
mentionEachUserInMessage({
message: payload.message,
userIds: payload.usersWithMatchingRole,
channelId: payload.channelId,
env,
})
);
return discordTextResponse(
`Found ${usersWithMatchingRole.length} users with matched role, mentioning them shortly...`
);
}
}
12 changes: 8 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ router.delete("/roles", removeGuildRoleHandler);

router.post("/profile/blocked", sendProfileBlockedMessage);

router.post("/", async (request, env) => {
router.post("/", async (request, env, ctx: ExecutionContext) => {
const message: discordMessageRequest = await request.json();

if (message.type === InteractionType.PING) {
Expand All @@ -66,7 +66,7 @@ router.post("/", async (request, env) => {
});
}
if (message.type === InteractionType.APPLICATION_COMMAND) {
return baseHandler(message, env);
return baseHandler(message, env, ctx);
}
return new JSONResponse(response.UNKNOWN_INTERACTION, { status: 400 });
});
Expand All @@ -78,7 +78,11 @@ router.all("*", async () => {
});

export default {
async fetch(request: Request, env: env): Promise<Response> {
async fetch(
request: Request,
env: env,
ctx: ExecutionContext
): Promise<Response> {
const apiUrls = ["/invite", "/roles", "/profile/blocked"];
const url = new URL(request.url);
if (request.method === "POST" && !apiUrls.includes(url.pathname)) {
Expand All @@ -87,7 +91,7 @@ export default {
return new JSONResponse(response.BAD_SIGNATURE, { status: 401 });
}
}
return router.handle(request, env);
return router.handle(request, env, ctx);
},

async scheduled(req: Request, env: env, ctx: ExecutionContext) {
Expand Down
53 changes: 53 additions & 0 deletions src/typeDefinitions/discordMessage.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,59 @@ export interface discordMessageRequest {
data: messageRequestData;
member: messageRequestMember;
guild_id: number;
channel_id: number;
}

export interface DiscordMessageResponse {
id: string;
type: number;
content: string;
channel_id: string;
author: {
id: string;
username: string;
avatar: string | null;
discriminator: string;
public_flags: number;
premium_type: number;
flags: number;
bot: boolean;
banner: string | null;
accent_color: string | null;
global_name: string | null;
avatar_decoration_data: string | null;
banner_color: string | null;
};
attachments: Array<string>;
embeds: Array<string>;
mentions: {
id: string;
username: string;
avatar: string | null;
discriminator: string;
public_flags: number;
premium_type: number;
flags: number;
banner: string | null;
accent_color: string | null;
global_name: string | null;
avatar_decoration_data: string | null;
banner_color: string | null;
}[];
mention_roles: Array<string>;
pinned: boolean;
mention_everyone: boolean;
tts: boolean;
timestamp: string;
edited_timestamp: string | null;
flags: number;
components: Array<string>;
referenced_message: Arra<string> | null;
}

export interface discordMessageError {
code: number;
message: string;
}

export interface messageRequestData {
Expand Down
5 changes: 5 additions & 0 deletions src/typeDefinitions/filterUsersByRole.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ export type MentionEachUserOptions = {
type: number;
value: string;
};
export type DevFlag = {
name: string;
type: number;
value: boolean;
};
4 changes: 2 additions & 2 deletions src/utils/batchDiscordRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ 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"
);
rateLimitRemaining = Math.floor(rateLimitRemaining * (1 - LIMIT_BUFFER));
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"
);
Expand Down
73 changes: 73 additions & 0 deletions src/utils/guildRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ 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 {
createNewRole,
discordMessageError,
discordMessageRequest,
guildRoleResponse,
memberGroupRole,
} from "../typeDefinitions/discordMessage.types";
import { GuildRole, Role } from "../typeDefinitions/role.types";
import createDiscordHeaders from "./createDiscordHeaders";
import { sleep } from "./sleep";

export async function createGuildRole(
body: createNewRole,
Expand Down Expand Up @@ -135,3 +143,68 @@ export async function getGuildRoleByName(
const roles = await getGuildRoles(env);
return roles?.find((role) => role.name === roleName);
}

export async function mentionEachUserInMessage({
message,
userIds,
channelId,
env,
}: {
message?: string;
userIds: string[];
channelId: number;
env: env;
}) {
const batchSize = 5;
let waitTillNextAPICall = 0;
try {
const failedUsers: Array<string> = [];
for (let i = 0; i < userIds.length; i += batchSize) {
const batchwiseUserIds = userIds.slice(i, i + batchSize);
const messageRequest = batchwiseUserIds.map((userId) => {
return fetch(`${DISCORD_BASE_URL}/channels/${channelId}/messages`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${env.DISCORD_TOKEN}`,
},
body: JSON.stringify({
content: `${message ? message + " " : ""} ${userId}`,
}),
}).then((response) => {
const rateLimitRemaining = parseRateLimitRemaining(response);
if (rateLimitRemaining === 0) {
waitTillNextAPICall = Math.max(
parseResetAfter(response),
waitTillNextAPICall
);
}
return response.json();
}) as Promise<discordMessageRequest | discordMessageError>;
});
const responses = await Promise.all(messageRequest);
responses.forEach((response, i) => {
if (response && "message" in response) {
failedUsers.push(batchwiseUserIds[i]);
console.error(`Failed to mention a user`);
}
});
await sleep(waitTillNextAPICall * 1000);
waitTillNextAPICall = 0;
}
if (failedUsers.length > 0) {
await fetch(`${DISCORD_BASE_URL}/channels/${channelId}/messages`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${env.DISCORD_TOKEN}`,
},
body: JSON.stringify({
content: `Failed to tag ${failedUsers} individually.`,
}),
});
}
} catch (error) {
console.log("Error occured while running mentionEachUserInMessage", error);
}
}
5 changes: 5 additions & 0 deletions src/utils/sleep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function sleep(delay = 1000) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
Loading

0 comments on commit bc327ba

Please sign in to comment.