Skip to content

Commit

Permalink
Merge pull request #221 from Real-Dev-Squad/develop
Browse files Browse the repository at this point in the history
Dev to main sync
  • Loading branch information
iamitprakash authored Apr 18, 2024
2 parents ea07431 + 4aec478 commit 05fccd0
Show file tree
Hide file tree
Showing 18 changed files with 597 additions and 52 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"lint-fix": "eslint --fix .",
"format-check": "echo 'Checking the formatting of your code 👨‍💻' && prettier --check . && echo '✅ code matches prettier formatting'",
"format-fix": "prettier --write .",
"fix": "npm run lint-fix && npm run format-fix",
"ngrok": "ngrok http 8787",
"register": "ts-node-esm src/register.ts"
},
Expand Down
13 changes: 13 additions & 0 deletions src/constants/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ export const MENTION_EACH = {
],
};

export const REMOVE = {
name: "remove",
description: "remove user/users from the server",
options: [
{
name: "role",
description: "remove developers with specific role",
type: 8, // User type
required: true,
},
],
};

export const LISTENING = {
name: "listening",
description: "mark user as listening",
Expand Down
11 changes: 11 additions & 0 deletions src/controllers/baseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
NOTIFY_ONBOARDING,
OOO,
USER,
REMOVE,
} from "../constants/commands";
import { updateNickName } from "../utils/updateNickname";
import { discordEphemeralResponse } from "../utils/discordEphemeralResponse";
Expand All @@ -40,6 +41,7 @@ import {
RETRY_COMMAND,
} from "../constants/responses";
import { DevFlag } from "../typeDefinitions/filterUsersByRole";
import { kickEachUser } from "./kickEachUser";

export async function baseHandler(
message: discordMessageRequest,
Expand Down Expand Up @@ -75,6 +77,15 @@ export async function baseHandler(
return await mentionEachUser(transformedArgument, env, ctx);
}

case getCommandName(REMOVE): {
const data = message.data?.options as Array<messageRequestDataOptions>;
const transformedArgument = {
roleToBeRemovedObj: data[0],
channelId: message.channel_id,
};
return await kickEachUser(transformedArgument, env, ctx);
}

case getCommandName(LISTENING): {
const data = message.data?.options;
const setter = data ? data[0].value : false;
Expand Down
44 changes: 44 additions & 0 deletions src/controllers/kickEachUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
MentionEachUserOptions,
UserArray,
} from "../typeDefinitions/filterUsersByRole";
import { env } from "../typeDefinitions/default.types";
import { getMembersInServer } from "../utils/getMembersInServer";
import { filterUserByRoles } from "../utils/filterUsersByRole";
import { discordTextResponse } from "../utils/discordResponse";
import { removeUsers } from "../utils/removeUsers";

export async function kickEachUser(
transformedArgument: {
roleToBeRemovedObj: MentionEachUserOptions;
channelId: number;
},
env: env,
ctx: ExecutionContext
) {
const getMembersInServerResponse = await getMembersInServer(env);
const roleId = transformedArgument.roleToBeRemovedObj.value;

const usersWithMatchingRole = filterUserByRoles(
getMembersInServerResponse as UserArray[],
roleId
);

const usersText = usersWithMatchingRole
.map((user) => {
return user;
})
.join("\n");

if (usersWithMatchingRole.length === 0) {
return discordTextResponse(
`We couldn't find any user(s) assigned to <@&${roleId}> role.`
);
} else {
ctx.waitUntil(
removeUsers(env, usersWithMatchingRole, transformedArgument.channelId)
);
const responseText = `Following users will be removed shortly ..... :\n${usersText}`;
return discordTextResponse(responseText);
}
}
29 changes: 29 additions & 0 deletions src/controllers/taskUpdatesHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { env } from "../typeDefinitions/default.types";
import JSONResponse from "../utils/JsonResponse";
import * as response from "../constants/responses";
import { verifyNodejsBackendAuthToken } from "../utils/verifyAuthToken";
import { sendTaskUpdate } from "../utils/sendTaskUpdates";
import { TaskUpdates } from "../typeDefinitions/taskUpdate";
import { IRequest } from "itty-router";

export const sendTaskUpdatesHandler = async (request: IRequest, env: env) => {
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
return new JSONResponse(response.UNAUTHORIZED, { status: 401 });
}
try {
await verifyNodejsBackendAuthToken(authHeader, env);
const updates: TaskUpdates = await request.json();
const { completed, planned, blockers } = updates.content;
await sendTaskUpdate(completed, planned, blockers, env);
return new JSONResponse(
"Task update sent on Discord's tracking-updates channel."
);
} catch (error: any) {
return new JSONResponse({
res: response.INTERNAL_SERVER_ERROR,
message: error.message,
status: 500,
});
}
};
20 changes: 11 additions & 9 deletions src/handlers/scheduledEventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { env } from "../typeDefinitions/default.types";
import { taskOverDueDiscordMembers } from "../utils/taskOverDueDiscordMembers";
import * as error from "../constants/responses";
import { getDiscordIds } from "../utils/getDiscordIds";
import config from "../../config/config";
import { SUPER_USER_ONE, SUPER_USER_TWO } from "../constants/variables";

export async function send(env: env): Promise<void> {
try {
const assigneeIds: string[] | string = await taskOverDueDiscordMembers();
const discordIds: string[] = await taskOverDueDiscordMembers();

//A user might have more than one task which are running red
//so to mention them just once, we are using Set to filter out
const discordIds: string[] | string = await getDiscordIds(assigneeIds);
const uniqueDiscordIds = [...new Set(discordIds)];
const superUsers = [SUPER_USER_ONE, SUPER_USER_TWO];
const filteredDiscordIds = discordIds.filter(
(id) => !superUsers.includes(id)
);

if (filteredDiscordIds.length === 0) {
return;
}

//notifying the two users with the authority.
let stringToBeSent = `<@${SUPER_USER_ONE}> <@${SUPER_USER_TWO}>\nThese people have their task running red:\n`;

let forFormatting = 0;
uniqueDiscordIds.forEach((id) => {
filteredDiscordIds.forEach((id: string) => {
const discordUser = `<@${id}> `;
stringToBeSent += discordUser;
forFormatting++;
Expand All @@ -35,7 +37,7 @@ export async function send(env: env): Promise<void> {

const url = config(env).TRACKING_CHANNEL_URL;

const res = await fetch(url, {
await fetch(url, {
method: "POST",
body: JSON.stringify(bodyObj),
headers: {
Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { getGuildMemberDetailsHandler } from "./controllers/getGuildMemberDetail
import { send } from "./handlers/scheduledEventHandler";
import { generateInviteLink } from "./controllers/generateDiscordInvite";
import { sendProfileBlockedMessage } from "./controllers/profileHandler";
import { sendTaskUpdatesHandler } from "./controllers/taskUpdatesHandler";

const router = Router();

Expand Down Expand Up @@ -57,6 +58,8 @@ router.delete("/roles", removeGuildRoleHandler);

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

router.post("/task/update", sendTaskUpdatesHandler);

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

Expand All @@ -83,7 +86,7 @@ export default {
env: env,
ctx: ExecutionContext
): Promise<Response> {
const apiUrls = ["/invite", "/roles", "/profile/blocked"];
const apiUrls = ["/invite", "/roles", "/profile/blocked", "/task/update"];
const url = new URL(request.url);
if (request.method === "POST" && !apiUrls.includes(url.pathname)) {
const isVerifiedRequest = await verifyBot(request, env);
Expand Down
2 changes: 2 additions & 0 deletions src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
NOTIFY_ONBOARDING,
OOO,
USER,
REMOVE,
} from "./constants/commands";
import { config } from "dotenv";
import { DISCORD_BASE_URL } from "./constants/urls";
Expand Down Expand Up @@ -37,6 +38,7 @@ async function registerGuildCommands(
USER,
NOTIFY_OVERDUE,
NOTIFY_ONBOARDING,
REMOVE,
];

try {
Expand Down
7 changes: 7 additions & 0 deletions src/typeDefinitions/taskUpdate.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface TaskUpdates {
content: {
completed: string;
planned: string;
blockers: string;
};
}
76 changes: 76 additions & 0 deletions src/utils/removeUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { env } from "../typeDefinitions/default.types";
import { DISCORD_BASE_URL } from "../constants/urls";
import {
parseRateLimitRemaining,
parseResetAfter,
} from "./batchDiscordRequests";
import {
discordMessageRequest,
discordMessageError,
} from "../typeDefinitions/discordMessage.types";

export async function removeUsers(
env: env,
usersWithMatchingRole: string[],
channelId: number
) {
const batchSize = 4;
let waitTillNextAPICall = 0;

try {
const failedUsers: Array<string> = [];
for (let i = 0; i < usersWithMatchingRole.length; i += batchSize) {
const batchwiseUsers = usersWithMatchingRole.slice(i, i + batchSize);
const deleteRequests = batchwiseUsers.map((mention) => {
const userId = mention.replace(/<@!*/g, "").replace(/>/g, "");
const url = `${DISCORD_BASE_URL}/guilds/${env.DISCORD_GUILD_ID}/members/${userId}`;

return fetch(url, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${env.DISCORD_TOKEN}`,
},
}).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(deleteRequests);
responses.forEach((response, i) => {
if (response && "message" in response) {
failedUsers.push(batchwiseUsers[i]);
console.error(`Failed to remove 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 remove ${failedUsers}.`,
}),
});
}
} catch (error) {
console.error("Error occurred while removing users:", error);
}
}

function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
34 changes: 34 additions & 0 deletions src/utils/sendTaskUpdates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { env } from "../typeDefinitions/default.types";
import config from "../../config/config";

export async function sendTaskUpdate(
completed: string,
planned: string,
blockers: string,
env: env
): Promise<void> {
const formattedString = `**Completed**: ${completed}\n\n**Planned**: ${planned}\n\n**Blockers**: ${blockers}`;
const bodyObj = {
content: formattedString,
};
const url = config(env).TRACKING_CHANNEL_URL;
try {
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(bodyObj),
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${env.DISCORD_TOKEN}`,
},
});

if (!response.ok) {
throw new Error(
`Failed to send task update: ${response.status} - ${response.statusText}`
);
}
} catch (error) {
console.error("Error occurred while sending task update:", error);
throw error;
}
}
20 changes: 7 additions & 13 deletions src/utils/taskOverDueDiscordMembers.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import { RDS_BASE_API_URL } from "../constants/urls";
import {
TaskOverdue,
TaskOverdueResponse,
} from "../typeDefinitions/taskOverdue.types";
import * as errors from "../constants/responses";
import { UserOverdueTaskResponseType } from "../typeDefinitions/rdsUser";

export const taskOverDueDiscordMembers = async (): Promise<
string[] | string
> => {
export const taskOverDueDiscordMembers = async (): Promise<string[]> => {
try {
const overDueUrl = `${RDS_BASE_API_URL}/tasks?dev=true&status=overdue&size=100`;
const overDueUrl = `${RDS_BASE_API_URL}/users?query=filterBy:overdue_tasks`;

const response: Response = await fetch(overDueUrl);
const responseObj: TaskOverdueResponse = await response.json();
const responseObj: UserOverdueTaskResponseType = await response.json();

const assigneeIds: string[] = responseObj.tasks.map(
(task: TaskOverdue) => task.assigneeId
const discordIds: string[] = responseObj.users.map(
(user) => user.discordId
);

return assigneeIds;
return discordIds;
} catch (e) {
console.log(e);
throw e;
Expand Down
Loading

0 comments on commit 05fccd0

Please sign in to comment.