Skip to content

Commit a25ccb4

Browse files
Merge pull request #294 from pankajjs/feat/create-onboarding-extension-command
feat: Add onboarding extension command to create an onboarding extension request
2 parents 6d99d4f + 532e48f commit a25ccb4

File tree

12 files changed

+423
-0
lines changed

12 files changed

+423
-0
lines changed

src/constants/commands.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,34 @@ export const NOTIFY_ONBOARDING = {
217217
},
218218
],
219219
};
220+
221+
export const ONBOARDING_EXTENSION = {
222+
name: "onboarding-extension",
223+
description: "This command helps to create an onboarding extension request.",
224+
options: [
225+
{
226+
name: "number-of-days",
227+
description: "Number of days required for the extension request",
228+
type: 4,
229+
required: true,
230+
},
231+
{
232+
name: "reason",
233+
description: "Reason for the extension request",
234+
type: 3,
235+
required: true,
236+
},
237+
{
238+
name: "username",
239+
description: "Username of onboarding user",
240+
type: 6,
241+
required: false,
242+
},
243+
{
244+
name: "dev",
245+
description: "Feature flag",
246+
type: 5,
247+
required: false,
248+
},
249+
],
250+
};

src/constants/responses.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,5 @@ export const AUTHENTICATION_ERROR = "Invalid Authentication token";
8383
export const TASK_UPDATE_SENT_MESSAGE =
8484
"Task update sent on Discord's tracking-updates channel.";
8585
export const NOT_IMPLEMENTED = "Feature not implemented";
86+
export const UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST =
87+
"Only super user and onboarding user are authorized to create an onboarding extension request";

src/controllers/baseHandler.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
REMOVE,
3131
GROUP_INVITE,
3232
GRANT_AWS_ACCESS,
33+
ONBOARDING_EXTENSION,
3334
} from "../constants/commands";
3435
import { updateNickName } from "../utils/updateNickname";
3536
import { discordEphemeralResponse } from "../utils/discordEphemeralResponse";
@@ -46,6 +47,7 @@ import { DevFlag } from "../typeDefinitions/filterUsersByRole";
4647
import { kickEachUser } from "./kickEachUser";
4748
import { groupInvite } from "./groupInvite";
4849
import { grantAWSAccessCommand } from "./grantAWSAccessCommand";
50+
import { onboardingExtensionCommand } from "./onboardingExtensionCommand";
4951

5052
export async function baseHandler(
5153
message: discordMessageRequest,
@@ -187,6 +189,21 @@ export async function baseHandler(
187189

188190
return await groupInvite(data[0].value, data[1].value, env);
189191
}
192+
193+
case getCommandName(ONBOARDING_EXTENSION): {
194+
const data = message.data?.options as Array<messageRequestDataOptions>;
195+
const transformedArgument = {
196+
numberOfDaysObj: data[0],
197+
reasonObj: data[1],
198+
userIdObj: data.find((item) => item.name === "username"),
199+
channelId: message.channel_id,
200+
memberObj: message.member,
201+
devObj: data.find((item) => item.name === "dev") as unknown as DevFlag,
202+
};
203+
204+
return await onboardingExtensionCommand(transformedArgument, env, ctx);
205+
}
206+
190207
default: {
191208
return commandNotFound();
192209
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { env } from "../typeDefinitions/default.types";
2+
import {
3+
messageRequestDataOptions,
4+
messageRequestMember,
5+
} from "../typeDefinitions/discordMessage.types";
6+
import { DevFlag } from "../typeDefinitions/filterUsersByRole";
7+
import { discordTextResponse } from "../utils/discordResponse";
8+
import {
9+
createOnboardingExtension,
10+
CreateOnboardingExtensionArgs,
11+
} from "../utils/onboardingExtension";
12+
13+
export async function onboardingExtensionCommand(
14+
transformedArgument: {
15+
memberObj: messageRequestMember;
16+
userIdObj?: messageRequestDataOptions;
17+
numberOfDaysObj: messageRequestDataOptions;
18+
reasonObj: messageRequestDataOptions;
19+
channelId: number;
20+
devObj?: DevFlag;
21+
},
22+
env: env,
23+
ctx: ExecutionContext
24+
) {
25+
const dev = transformedArgument.devObj?.value || false;
26+
const discordId = transformedArgument.memberObj.user.id.toString();
27+
28+
if (!dev) {
29+
return discordTextResponse(`<@${discordId}> Feature not implemented`);
30+
}
31+
32+
const args: CreateOnboardingExtensionArgs = {
33+
channelId: transformedArgument.channelId,
34+
userId: transformedArgument.userIdObj?.value,
35+
numberOfDays: Number(transformedArgument.numberOfDaysObj.value),
36+
reason: transformedArgument.reasonObj.value,
37+
discordId: discordId,
38+
};
39+
40+
const initialResponse = `<@${discordId}> Processing your request for onboarding extension`;
41+
42+
ctx.waitUntil(createOnboardingExtension(args, env));
43+
44+
return discordTextResponse(initialResponse);
45+
}

src/register.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
REMOVE,
1212
GROUP_INVITE,
1313
GRANT_AWS_ACCESS,
14+
ONBOARDING_EXTENSION,
1415
} from "./constants/commands";
1516
import { config } from "dotenv";
1617
import { DISCORD_BASE_URL } from "./constants/urls";
@@ -44,6 +45,7 @@ async function registerGuildCommands(
4445
REMOVE,
4546
GROUP_INVITE,
4647
GRANT_AWS_ACCESS,
48+
ONBOARDING_EXTENSION,
4749
];
4850

4951
try {

src/typeDefinitions/rdsUser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type UserType = {
77
archived: boolean;
88
in_discord: boolean;
99
member?: boolean;
10+
super_user?: boolean;
1011
};
1112
created_at?: number;
1213
yoe?: number;

src/utils/onboardingExtension.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import config from "../../config/config";
2+
import { UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST } from "../constants/responses";
3+
import { DISCORD_BASE_URL } from "../constants/urls";
4+
import { env } from "../typeDefinitions/default.types";
5+
import { generateDiscordAuthToken } from "./authTokenGenerator";
6+
import { getUserDetails } from "./getUserDetails";
7+
import { sendReplyInDiscordChannel } from "./sendReplyInDiscordChannel";
8+
9+
export type CreateOnboardingExtensionArgs = {
10+
userId?: string;
11+
channelId: number;
12+
reason: string;
13+
numberOfDays: number;
14+
discordId: string;
15+
};
16+
17+
export const createOnboardingExtension = async (
18+
args: CreateOnboardingExtensionArgs,
19+
env: env
20+
) => {
21+
const { channelId } = args;
22+
23+
const authToken = await generateDiscordAuthToken(
24+
"Cloudflare Worker",
25+
Math.floor(Date.now() / 1000) + 2,
26+
env.BOT_PRIVATE_KEY,
27+
"RS256"
28+
);
29+
30+
let content: string;
31+
const discordReplyUrl = `${DISCORD_BASE_URL}/channels/${channelId}/messages`;
32+
33+
if (args.userId && args.discordId !== args.userId) {
34+
const userResponse = await getUserDetails(args.discordId);
35+
if (!userResponse?.user?.roles?.super_user) {
36+
content = `<@${args.discordId}> ${UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST}`;
37+
return await sendReplyInDiscordChannel(discordReplyUrl, content, env);
38+
}
39+
}
40+
41+
const userDiscordId = args.userId ? args.userId : args.discordId;
42+
const base_url = config(env).RDS_BASE_API_URL;
43+
const createOnboardingExtensionUrl = `${base_url}/requests?dev=true`;
44+
45+
const requestBody = {
46+
userId: userDiscordId,
47+
type: "ONBOARDING",
48+
numberOfDays: args.numberOfDays,
49+
reason: args.reason,
50+
};
51+
52+
try {
53+
const response = await fetch(createOnboardingExtensionUrl, {
54+
method: "POST",
55+
headers: {
56+
"Content-Type": "application/json",
57+
Authorization: `Bearer ${authToken}`,
58+
},
59+
body: JSON.stringify(requestBody),
60+
});
61+
const jsonResponse = (await response.json()) as unknown as {
62+
message: string;
63+
};
64+
content = `<@${args.discordId}> ${jsonResponse.message}`;
65+
return await sendReplyInDiscordChannel(discordReplyUrl, content, env);
66+
} catch (err) {
67+
content = `<@${args.discordId}> Error occurred while creating onboarding extension request.`;
68+
return await sendReplyInDiscordChannel(discordReplyUrl, content, env);
69+
}
70+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { env } from "../typeDefinitions/default.types";
2+
3+
export const sendReplyInDiscordChannel = async (
4+
discordReplyUrl: string,
5+
body: any,
6+
env: env
7+
) => {
8+
await fetch(discordReplyUrl, {
9+
method: "POST",
10+
headers: {
11+
"Content-Type": "application/json",
12+
Authorization: `Bot ${env.DISCORD_TOKEN}`,
13+
},
14+
body: JSON.stringify({
15+
content: body,
16+
}),
17+
});
18+
};

tests/fixtures/fixture.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,3 +383,21 @@ export const testDataWithDevTitle = {
383383
value: true,
384384
},
385385
};
386+
387+
export const transformedArgsForOnboardingExtension = {
388+
memberObj: {
389+
user: {
390+
id: 134672111,
391+
username: "username",
392+
discriminator: "<discriminator>",
393+
avatar: "<avatar>",
394+
},
395+
nick: "<nick>",
396+
joined_at: "<joined_at>",
397+
},
398+
userIdObj: { name: "userId", type: 6, value: "1545562672", options: [] },
399+
numberOfDaysObj: { value: "20", name: "numberOfDays", type: 3, options: [] },
400+
reasonObj: { value: "reason", name: "reason", type: 3, options: [] },
401+
channelId: 6754321,
402+
devObj: { value: false, name: "dev", type: 5 },
403+
};
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { onboardingExtensionCommand } from "../../../src/controllers/onboardingExtensionCommand";
2+
import { discordTextResponse } from "../../../src/utils/discordResponse";
3+
import {
4+
ctx,
5+
guildEnv,
6+
transformedArgsForOnboardingExtension,
7+
} from "../../fixtures/fixture";
8+
import * as utils from "../../../src/utils/onboardingExtension";
9+
10+
describe("onboardingExtensionCommand", () => {
11+
beforeEach(() => {
12+
jest.clearAllMocks();
13+
});
14+
afterEach(() => {
15+
jest.restoreAllMocks();
16+
});
17+
const discordId =
18+
transformedArgsForOnboardingExtension.memberObj.user.id.toString();
19+
20+
it("should return Feature not implemented", async () => {
21+
const expectedRes = await onboardingExtensionCommand(
22+
transformedArgsForOnboardingExtension,
23+
guildEnv,
24+
ctx
25+
);
26+
const jsonResponse = await expectedRes.json();
27+
const mockResponse = discordTextResponse(
28+
`<@${discordId}> Feature not implemented`
29+
);
30+
const mockJsonResponse = await mockResponse.json();
31+
expect(jsonResponse).toStrictEqual(mockJsonResponse);
32+
});
33+
34+
it("should return initial response", async () => {
35+
transformedArgsForOnboardingExtension.devObj.value = true;
36+
const expectedRes = await onboardingExtensionCommand(
37+
transformedArgsForOnboardingExtension,
38+
guildEnv,
39+
ctx
40+
);
41+
const jsonResponse = await expectedRes.json();
42+
const mockResponse = discordTextResponse(
43+
`<@${discordId}> Processing your request for onboarding extension`
44+
);
45+
const mockJsonResponse = await mockResponse.json();
46+
expect(jsonResponse).toStrictEqual(mockJsonResponse);
47+
});
48+
49+
it("should call createOnboardingExtension", async () => {
50+
jest.spyOn(utils, "createOnboardingExtension");
51+
transformedArgsForOnboardingExtension.devObj.value = true;
52+
await onboardingExtensionCommand(
53+
transformedArgsForOnboardingExtension,
54+
guildEnv,
55+
ctx
56+
);
57+
expect(utils.createOnboardingExtension).toHaveBeenCalledTimes(1);
58+
expect(utils.createOnboardingExtension).toHaveBeenCalledWith(
59+
{
60+
channelId: transformedArgsForOnboardingExtension.channelId,
61+
userId: transformedArgsForOnboardingExtension.userIdObj?.value,
62+
numberOfDays: Number(
63+
transformedArgsForOnboardingExtension.numberOfDaysObj.value
64+
),
65+
reason: transformedArgsForOnboardingExtension.reasonObj.value,
66+
discordId: discordId,
67+
},
68+
guildEnv
69+
);
70+
});
71+
});

0 commit comments

Comments
 (0)