Skip to content

Commit a5010fb

Browse files
committed
added feature to remove tagged users of a particular role
1 parent c12626c commit a5010fb

File tree

7 files changed

+222
-0
lines changed

7 files changed

+222
-0
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 KICK = {
38+
name: "kick",
39+
description: "Kick a user from the server",
40+
options: [
41+
{
42+
name: "role",
43+
description: "The role to kick",
44+
type: 8, // User type
45+
required: false,
46+
},
47+
],
48+
};
49+
3750
export const LISTENING = {
3851
name: "listening",
3952
description: "mark user as listening",

src/controllers/baseHandler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
NOTIFY_ONBOARDING,
2828
OOO,
2929
USER,
30+
KICK,
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,
@@ -48,6 +50,8 @@ export async function baseHandler(
4850
): Promise<JSONResponse> {
4951
const command = lowerCaseMessageCommands(message);
5052

53+
console.log("Message: ", JSON.stringify(message.data));
54+
console.log("Envior:", env);
5155
switch (command) {
5256
case getCommandName(HELLO): {
5357
return helloCommand(message.member.user.id);
@@ -75,6 +79,14 @@ export async function baseHandler(
7579
return await mentionEachUser(transformedArgument, env, ctx);
7680
}
7781

82+
case getCommandName(KICK): {
83+
const data = message.data?.options as Array<messageRequestDataOptions>;
84+
const transformedArgument = {
85+
roleToBeRemovedObj: data[0],
86+
};
87+
return await kickEachUser(transformedArgument, env, ctx);
88+
}
89+
7890
case getCommandName(LISTENING): {
7991
const data = message.data?.options;
8092
const setter = data ? data[0].value : false;

src/controllers/kickEachUser.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
},
15+
env: env,
16+
ctx: ExecutionContext
17+
) {
18+
const getMembersInServerResponse = await getMembersInServer(env);
19+
const roleId = transformedArgument.roleToBeRemovedObj.value;
20+
21+
const usersWithMatchingRole = filterUserByRoles(
22+
getMembersInServerResponse as UserArray[],
23+
roleId
24+
);
25+
26+
if (usersWithMatchingRole.length === 0) {
27+
return discordTextResponse(`Found no users with the matched role.`);
28+
} else {
29+
ctx.waitUntil(removeUsers(env, usersWithMatchingRole));
30+
return discordTextResponse(
31+
`Found ${usersWithMatchingRole.length} users with the matched role, removing them shortly...`
32+
);
33+
}
34+
}

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+
KICK,
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+
KICK,
4042
];
4143

4244
try {

src/utils/removeUsers.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { env } from "../typeDefinitions/default.types";
2+
import { DISCORD_BASE_URL } from "../constants/urls";
3+
4+
export const removeUsers = async (
5+
env: env,
6+
usersWithMatchingRole: string[]
7+
) => {
8+
const baseUrl = `${DISCORD_BASE_URL}/guilds/${env.DISCORD_GUILD_ID}/members`;
9+
// Method : DELETE /guilds/{guild.id}/members/{user.id}
10+
11+
for (const mention of usersWithMatchingRole) {
12+
// Remove <@ and > symbols from the mention
13+
const userId = mention.replace(/<@!*/g, "").replace(/>/g, "");
14+
const url = `${baseUrl}/${userId}`;
15+
16+
const headers = {
17+
"Content-Type": "application/json",
18+
Authorization: `Bot ${env.DISCORD_TOKEN}`,
19+
};
20+
21+
try {
22+
await fetch(url, { method: "DELETE", headers });
23+
} catch (error) {
24+
console.error(`Error removing user with ID ${userId}:`, error);
25+
}
26+
}
27+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { kickEachUser } from "../../../src/controllers/kickEachUser";
2+
import { discordTextResponse } from "../../../src/utils/discordResponse";
3+
import { removeUsers } from "../../../src/utils/removeUsers";
4+
import { transformedArgument, ctx } from "../../fixtures/fixture";
5+
6+
describe("kickEachUser", () => {
7+
it("should run when found no users with Matched Role", async () => {
8+
const env = {
9+
BOT_PUBLIC_KEY: "xyz",
10+
DISCORD_GUILD_ID: "123",
11+
DISCORD_TOKEN: "abc",
12+
};
13+
14+
const { roleToBeTaggedObj } = transformedArgument; // Extracting roleToBeTaggedObj
15+
const response = kickEachUser(
16+
{ roleToBeRemovedObj: roleToBeTaggedObj },
17+
env,
18+
ctx
19+
);
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+
"Found no users with the matched role."
28+
);
29+
});
30+
31+
it("should run when found users with Matched Role", async () => {
32+
const env = {
33+
BOT_PUBLIC_KEY: "xyz",
34+
DISCORD_GUILD_ID: "123",
35+
DISCORD_TOKEN: "abc",
36+
};
37+
38+
const usersWithMatchingRole = [
39+
"<@282859044593598464>",
40+
"<@725745030706364447>",
41+
] as string[];
42+
43+
const { roleToBeTaggedObj } = transformedArgument; // Extracting roleToBeTaggedObj
44+
const response = kickEachUser(
45+
{ roleToBeRemovedObj: roleToBeTaggedObj },
46+
env,
47+
ctx
48+
);
49+
50+
expect(response).toEqual(
51+
expect.objectContaining({
52+
data: {
53+
content:
54+
"Found 2 users with the matched role, removing them shortly...",
55+
},
56+
})
57+
); // Ensure correct response message
58+
59+
// Check the arguments passed to removeUsers
60+
expect(removeUsers).toHaveBeenCalledWith(env, usersWithMatchingRole);
61+
62+
expect(response).toBeInstanceOf(Promise);
63+
});
64+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { DISCORD_BASE_URL } from "../../../src/constants/urls";
2+
import JSONResponse from "../../../src/utils/JsonResponse";
3+
import { removeUsers } from "../../../src/utils/removeUsers";
4+
5+
describe("removeUsers", () => {
6+
const mockEnv = {
7+
BOT_PUBLIC_KEY: "xyz",
8+
DISCORD_GUILD_ID: "123",
9+
DISCORD_TOKEN: "abc",
10+
};
11+
12+
test("removes users successfully", async () => {
13+
const usersWithMatchingRole = ["<@userId1>", "<@userId2>"];
14+
15+
jest
16+
.spyOn(global, "fetch")
17+
.mockImplementation(() =>
18+
Promise.resolve(new Response(null, { status: 204 }))
19+
);
20+
await removeUsers(mockEnv, usersWithMatchingRole);
21+
22+
expect(fetch).toHaveBeenCalledTimes(2);
23+
expect(fetch).toHaveBeenCalledWith(
24+
`${DISCORD_BASE_URL}/guilds/${mockEnv.DISCORD_GUILD_ID}/members/userId1`,
25+
{
26+
method: "DELETE",
27+
headers: {
28+
"Content-Type": "application/json",
29+
Authorization: `Bot ${mockEnv.DISCORD_TOKEN}`,
30+
},
31+
}
32+
);
33+
expect(fetch).toHaveBeenCalledWith(
34+
`${DISCORD_BASE_URL}/guilds/${mockEnv.DISCORD_GUILD_ID}/members/userId2`,
35+
{
36+
method: "DELETE",
37+
headers: {
38+
"Content-Type": "application/json",
39+
Authorization: `Bot ${mockEnv.DISCORD_TOKEN}`,
40+
},
41+
}
42+
);
43+
});
44+
test("handles errors", async () => {
45+
const usersWithMatchingRole = ["<@userId1>"];
46+
47+
// Mocking the fetch function to simulate a rejected promise with a 404 error response
48+
jest
49+
.spyOn(global, "fetch")
50+
.mockImplementation(() =>
51+
Promise.reject(new Response(null, { status: 404 }))
52+
);
53+
54+
// Calling the function under test
55+
await removeUsers(mockEnv, usersWithMatchingRole);
56+
57+
// Expectations
58+
expect(fetch).toHaveBeenCalledTimes(3);
59+
expect(fetch).toHaveBeenCalledWith(
60+
`${DISCORD_BASE_URL}/guilds/${mockEnv.DISCORD_GUILD_ID}/members/userId1`,
61+
{
62+
method: "DELETE",
63+
headers: {
64+
"Content-Type": "application/json",
65+
Authorization: `Bot ${mockEnv.DISCORD_TOKEN}`,
66+
},
67+
}
68+
);
69+
});
70+
});

0 commit comments

Comments
 (0)