Skip to content

Commit 30150ac

Browse files
authored
Merge branch 'develop' into issue-219-online-users-filter
2 parents 9a116d2 + 3d82b14 commit 30150ac

File tree

13 files changed

+530
-1
lines changed

13 files changed

+530
-1
lines changed

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/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+
}

src/utils/sendTaskUpdates.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { env } from "../typeDefinitions/default.types";
2+
import config from "../../config/config";
3+
4+
export async function sendTaskUpdate(
5+
completed: string,
6+
planned: string,
7+
blockers: string,
8+
env: env
9+
): Promise<void> {
10+
const formattedString = `**Completed**: ${completed}\n\n**Planned**: ${planned}\n\n**Blockers**: ${blockers}`;
11+
const bodyObj = {
12+
content: formattedString,
13+
};
14+
const url = config(env).TRACKING_CHANNEL_URL;
15+
try {
16+
const response = await fetch(url, {
17+
method: "POST",
18+
body: JSON.stringify(bodyObj),
19+
headers: {
20+
"Content-Type": "application/json",
21+
Authorization: `Bot ${env.DISCORD_TOKEN}`,
22+
},
23+
});
24+
25+
if (!response.ok) {
26+
throw new Error(
27+
`Failed to send task update: ${response.status} - ${response.statusText}`
28+
);
29+
}
30+
} catch (error) {
31+
console.error("Error occurred while sending task update:", error);
32+
throw error;
33+
}
34+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { kickEachUser } from "../../../src/controllers/kickEachUser";
2+
import { transformedArgument, ctx } from "../../fixtures/fixture";
3+
4+
describe("kickEachUser", () => {
5+
it("should run when found no users with Matched Role", async () => {
6+
const env = {
7+
BOT_PUBLIC_KEY: "xyz",
8+
DISCORD_GUILD_ID: "123",
9+
DISCORD_TOKEN: "abc",
10+
};
11+
12+
const { roleToBeTaggedObj } = transformedArgument; // Extracting roleToBeTaggedObj
13+
const response = kickEachUser(
14+
{ roleToBeRemovedObj: roleToBeTaggedObj, channelId: 12345 },
15+
env,
16+
ctx
17+
);
18+
19+
const roleID = roleToBeTaggedObj.value;
20+
21+
expect(response).toBeInstanceOf(Promise);
22+
23+
const textMessage: { data: { content: string } } = await response.then(
24+
(res) => res.json()
25+
);
26+
expect(textMessage.data.content).toBe(
27+
`We couldn't find any user(s) assigned to <@&${roleID}> role.`
28+
);
29+
});
30+
});

0 commit comments

Comments
 (0)