Skip to content

Commit 05fccd0

Browse files
authored
Merge pull request #221 from Real-Dev-Squad/develop
Dev to main sync
2 parents ea07431 + 4aec478 commit 05fccd0

18 files changed

+597
-52
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"lint-fix": "eslint --fix .",
1414
"format-check": "echo 'Checking the formatting of your code 👨‍💻' && prettier --check . && echo '✅ code matches prettier formatting'",
1515
"format-fix": "prettier --write .",
16+
"fix": "npm run lint-fix && npm run format-fix",
1617
"ngrok": "ngrok http 8787",
1718
"register": "ts-node-esm src/register.ts"
1819
},

src/constants/commands.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ export const MENTION_EACH = {
3434
],
3535
};
3636

37+
export const REMOVE = {
38+
name: "remove",
39+
description: "remove user/users from the server",
40+
options: [
41+
{
42+
name: "role",
43+
description: "remove developers with specific role",
44+
type: 8, // User type
45+
required: true,
46+
},
47+
],
48+
};
49+
3750
export const LISTENING = {
3851
name: "listening",
3952
description: "mark user as listening",

src/controllers/baseHandler.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
NOTIFY_ONBOARDING,
2828
OOO,
2929
USER,
30+
REMOVE,
3031
} from "../constants/commands";
3132
import { updateNickName } from "../utils/updateNickname";
3233
import { discordEphemeralResponse } from "../utils/discordEphemeralResponse";
@@ -40,6 +41,7 @@ import {
4041
RETRY_COMMAND,
4142
} from "../constants/responses";
4243
import { DevFlag } from "../typeDefinitions/filterUsersByRole";
44+
import { kickEachUser } from "./kickEachUser";
4345

4446
export async function baseHandler(
4547
message: discordMessageRequest,
@@ -75,6 +77,15 @@ export async function baseHandler(
7577
return await mentionEachUser(transformedArgument, env, ctx);
7678
}
7779

80+
case getCommandName(REMOVE): {
81+
const data = message.data?.options as Array<messageRequestDataOptions>;
82+
const transformedArgument = {
83+
roleToBeRemovedObj: data[0],
84+
channelId: message.channel_id,
85+
};
86+
return await kickEachUser(transformedArgument, env, ctx);
87+
}
88+
7889
case getCommandName(LISTENING): {
7990
const data = message.data?.options;
8091
const setter = data ? data[0].value : false;

src/controllers/kickEachUser.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
MentionEachUserOptions,
3+
UserArray,
4+
} from "../typeDefinitions/filterUsersByRole";
5+
import { env } from "../typeDefinitions/default.types";
6+
import { getMembersInServer } from "../utils/getMembersInServer";
7+
import { filterUserByRoles } from "../utils/filterUsersByRole";
8+
import { discordTextResponse } from "../utils/discordResponse";
9+
import { removeUsers } from "../utils/removeUsers";
10+
11+
export async function kickEachUser(
12+
transformedArgument: {
13+
roleToBeRemovedObj: MentionEachUserOptions;
14+
channelId: number;
15+
},
16+
env: env,
17+
ctx: ExecutionContext
18+
) {
19+
const getMembersInServerResponse = await getMembersInServer(env);
20+
const roleId = transformedArgument.roleToBeRemovedObj.value;
21+
22+
const usersWithMatchingRole = filterUserByRoles(
23+
getMembersInServerResponse as UserArray[],
24+
roleId
25+
);
26+
27+
const usersText = usersWithMatchingRole
28+
.map((user) => {
29+
return user;
30+
})
31+
.join("\n");
32+
33+
if (usersWithMatchingRole.length === 0) {
34+
return discordTextResponse(
35+
`We couldn't find any user(s) assigned to <@&${roleId}> role.`
36+
);
37+
} else {
38+
ctx.waitUntil(
39+
removeUsers(env, usersWithMatchingRole, transformedArgument.channelId)
40+
);
41+
const responseText = `Following users will be removed shortly ..... :\n${usersText}`;
42+
return discordTextResponse(responseText);
43+
}
44+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { env } from "../typeDefinitions/default.types";
2+
import JSONResponse from "../utils/JsonResponse";
3+
import * as response from "../constants/responses";
4+
import { verifyNodejsBackendAuthToken } from "../utils/verifyAuthToken";
5+
import { sendTaskUpdate } from "../utils/sendTaskUpdates";
6+
import { TaskUpdates } from "../typeDefinitions/taskUpdate";
7+
import { IRequest } from "itty-router";
8+
9+
export const sendTaskUpdatesHandler = async (request: IRequest, env: env) => {
10+
const authHeader = request.headers.get("Authorization");
11+
if (!authHeader) {
12+
return new JSONResponse(response.UNAUTHORIZED, { status: 401 });
13+
}
14+
try {
15+
await verifyNodejsBackendAuthToken(authHeader, env);
16+
const updates: TaskUpdates = await request.json();
17+
const { completed, planned, blockers } = updates.content;
18+
await sendTaskUpdate(completed, planned, blockers, env);
19+
return new JSONResponse(
20+
"Task update sent on Discord's tracking-updates channel."
21+
);
22+
} catch (error: any) {
23+
return new JSONResponse({
24+
res: response.INTERNAL_SERVER_ERROR,
25+
message: error.message,
26+
status: 500,
27+
});
28+
}
29+
};

src/handlers/scheduledEventHandler.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import { env } from "../typeDefinitions/default.types";
22
import { taskOverDueDiscordMembers } from "../utils/taskOverDueDiscordMembers";
3-
import * as error from "../constants/responses";
4-
import { getDiscordIds } from "../utils/getDiscordIds";
53
import config from "../../config/config";
64
import { SUPER_USER_ONE, SUPER_USER_TWO } from "../constants/variables";
75

86
export async function send(env: env): Promise<void> {
97
try {
10-
const assigneeIds: string[] | string = await taskOverDueDiscordMembers();
8+
const discordIds: string[] = await taskOverDueDiscordMembers();
119

12-
//A user might have more than one task which are running red
13-
//so to mention them just once, we are using Set to filter out
14-
const discordIds: string[] | string = await getDiscordIds(assigneeIds);
15-
const uniqueDiscordIds = [...new Set(discordIds)];
10+
const superUsers = [SUPER_USER_ONE, SUPER_USER_TWO];
11+
const filteredDiscordIds = discordIds.filter(
12+
(id) => !superUsers.includes(id)
13+
);
14+
15+
if (filteredDiscordIds.length === 0) {
16+
return;
17+
}
1618

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

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

3638
const url = config(env).TRACKING_CHANNEL_URL;
3739

38-
const res = await fetch(url, {
40+
await fetch(url, {
3941
method: "POST",
4042
body: JSON.stringify(bodyObj),
4143
headers: {

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { getGuildMemberDetailsHandler } from "./controllers/getGuildMemberDetail
2020
import { send } from "./handlers/scheduledEventHandler";
2121
import { generateInviteLink } from "./controllers/generateDiscordInvite";
2222
import { sendProfileBlockedMessage } from "./controllers/profileHandler";
23+
import { sendTaskUpdatesHandler } from "./controllers/taskUpdatesHandler";
2324

2425
const router = Router();
2526

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

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

61+
router.post("/task/update", sendTaskUpdatesHandler);
62+
6063
router.post("/", async (request, env, ctx: ExecutionContext) => {
6164
const message: discordMessageRequest = await request.json();
6265

@@ -83,7 +86,7 @@ export default {
8386
env: env,
8487
ctx: ExecutionContext
8588
): Promise<Response> {
86-
const apiUrls = ["/invite", "/roles", "/profile/blocked"];
89+
const apiUrls = ["/invite", "/roles", "/profile/blocked", "/task/update"];
8790
const url = new URL(request.url);
8891
if (request.method === "POST" && !apiUrls.includes(url.pathname)) {
8992
const isVerifiedRequest = await verifyBot(request, env);

src/register.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
NOTIFY_ONBOARDING,
99
OOO,
1010
USER,
11+
REMOVE,
1112
} from "./constants/commands";
1213
import { config } from "dotenv";
1314
import { DISCORD_BASE_URL } from "./constants/urls";
@@ -37,6 +38,7 @@ async function registerGuildCommands(
3738
USER,
3839
NOTIFY_OVERDUE,
3940
NOTIFY_ONBOARDING,
41+
REMOVE,
4042
];
4143

4244
try {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface TaskUpdates {
2+
content: {
3+
completed: string;
4+
planned: string;
5+
blockers: string;
6+
};
7+
}

src/utils/removeUsers.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { env } from "../typeDefinitions/default.types";
2+
import { DISCORD_BASE_URL } from "../constants/urls";
3+
import {
4+
parseRateLimitRemaining,
5+
parseResetAfter,
6+
} from "./batchDiscordRequests";
7+
import {
8+
discordMessageRequest,
9+
discordMessageError,
10+
} from "../typeDefinitions/discordMessage.types";
11+
12+
export async function removeUsers(
13+
env: env,
14+
usersWithMatchingRole: string[],
15+
channelId: number
16+
) {
17+
const batchSize = 4;
18+
let waitTillNextAPICall = 0;
19+
20+
try {
21+
const failedUsers: Array<string> = [];
22+
for (let i = 0; i < usersWithMatchingRole.length; i += batchSize) {
23+
const batchwiseUsers = usersWithMatchingRole.slice(i, i + batchSize);
24+
const deleteRequests = batchwiseUsers.map((mention) => {
25+
const userId = mention.replace(/<@!*/g, "").replace(/>/g, "");
26+
const url = `${DISCORD_BASE_URL}/guilds/${env.DISCORD_GUILD_ID}/members/${userId}`;
27+
28+
return fetch(url, {
29+
method: "DELETE",
30+
headers: {
31+
"Content-Type": "application/json",
32+
Authorization: `Bot ${env.DISCORD_TOKEN}`,
33+
},
34+
}).then((response) => {
35+
const rateLimitRemaining = parseRateLimitRemaining(response);
36+
if (rateLimitRemaining === 0) {
37+
waitTillNextAPICall = Math.max(
38+
parseResetAfter(response),
39+
waitTillNextAPICall
40+
);
41+
}
42+
return response.json();
43+
}) as Promise<discordMessageRequest | discordMessageError>;
44+
});
45+
46+
const responses = await Promise.all(deleteRequests);
47+
responses.forEach((response, i) => {
48+
if (response && "message" in response) {
49+
failedUsers.push(batchwiseUsers[i]);
50+
console.error(`Failed to remove a user`);
51+
}
52+
});
53+
await sleep(waitTillNextAPICall * 1000);
54+
waitTillNextAPICall = 0;
55+
}
56+
57+
if (failedUsers.length > 0) {
58+
await fetch(`${DISCORD_BASE_URL}/channels/${channelId}/messages`, {
59+
method: "POST",
60+
headers: {
61+
"Content-Type": "application/json",
62+
Authorization: `Bot ${env.DISCORD_TOKEN}`,
63+
},
64+
body: JSON.stringify({
65+
content: `Failed to remove ${failedUsers}.`,
66+
}),
67+
});
68+
}
69+
} catch (error) {
70+
console.error("Error occurred while removing users:", error);
71+
}
72+
}
73+
74+
function sleep(ms: number) {
75+
return new Promise((resolve) => setTimeout(resolve, ms));
76+
}

0 commit comments

Comments
 (0)