Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ DISCORD_TOKEN: The token generated for your bot while creating a discord applica
DISCORD_PUBLIC_KEY: Public key of your Discord bot helps to verify the bot and apply interaction url
DISCORD_APPLICATION_ID: The application id of your bot.
DISCORD_GUILD_ID: Id of the guild where you want to install the slash commands.
AWS_READ_ACCESS_GROUP_ID: This we can have a random string for now, this is required to run the `/grant-aws-command` which can help to grant AWS access from discord. We pass two values the `username` and `aws-group-name`
```

To add more commands you need to modify following files:
Expand Down
31 changes: 31 additions & 0 deletions src/constants/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,34 @@ export const NOTIFY_ONBOARDING = {
},
],
};

export const ONBOARDING_EXTENSION = {
name: "onboarding-extension",
description: "This command helps to create an onboarding extension request.",
options: [
{
name: "number-of-days",
description: "Number of days required for the extension request",
type: 4,
required: true,
},
{
name: "reason",
description: "Reason for the extension request",
type: 3,
required: true,
},
{
name: "username",
description: "Username of onboarding user",
type: 6,
required: false,
},
{
name: "dev",
description: "Feature flag",
type: 5,
required: false,
},
],
};
2 changes: 2 additions & 0 deletions src/constants/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,5 @@ export const AUTHENTICATION_ERROR = "Invalid Authentication token";
export const TASK_UPDATE_SENT_MESSAGE =
"Task update sent on Discord's tracking-updates channel.";
export const NOT_IMPLEMENTED = "Feature not implemented";
export const UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST =
"Only super user and onboarding user are authorized to create an onboarding extension request";
17 changes: 17 additions & 0 deletions src/controllers/baseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
REMOVE,
GROUP_INVITE,
GRANT_AWS_ACCESS,
ONBOARDING_EXTENSION,
} from "../constants/commands";
import { updateNickName } from "../utils/updateNickname";
import { discordEphemeralResponse } from "../utils/discordEphemeralResponse";
Expand All @@ -46,6 +47,7 @@ import { DevFlag } from "../typeDefinitions/filterUsersByRole";
import { kickEachUser } from "./kickEachUser";
import { groupInvite } from "./groupInvite";
import { grantAWSAccessCommand } from "./grantAWSAccessCommand";
import { onboardingExtensionCommand } from "./onboardingExtensionCommand";

export async function baseHandler(
message: discordMessageRequest,
Expand Down Expand Up @@ -187,6 +189,21 @@ export async function baseHandler(

return await groupInvite(data[0].value, data[1].value, env);
}

case getCommandName(ONBOARDING_EXTENSION): {
const data = message.data?.options as Array<messageRequestDataOptions>;
const transformedArgument = {
numberOfDaysObj: data[0],
reasonObj: data[1],
userIdObj: data.find((item) => item.name === "username"),
channelId: message.channel_id,
memberObj: message.member,
devObj: data.find((item) => item.name === "dev") as unknown as DevFlag,
};

return await onboardingExtensionCommand(transformedArgument, env, ctx);
}

default: {
return commandNotFound();
}
Expand Down
45 changes: 45 additions & 0 deletions src/controllers/onboardingExtensionCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { env } from "../typeDefinitions/default.types";
import {
messageRequestDataOptions,
messageRequestMember,
} from "../typeDefinitions/discordMessage.types";
import { DevFlag } from "../typeDefinitions/filterUsersByRole";
import { discordTextResponse } from "../utils/discordResponse";
import {
createOnboardingExtension,
CreateOnboardingExtensionArgs,
} from "../utils/onboardingExtension";

export async function onboardingExtensionCommand(
transformedArgument: {
memberObj: messageRequestMember;
userIdObj?: messageRequestDataOptions;
numberOfDaysObj: messageRequestDataOptions;
reasonObj: messageRequestDataOptions;
channelId: number;
devObj?: DevFlag;
},
env: env,
ctx: ExecutionContext
) {
const dev = transformedArgument.devObj?.value || false;
const discordId = transformedArgument.memberObj.user.id.toString();

if (!dev) {
return discordTextResponse(`<@${discordId}> Feature not implemented`);
}

const args: CreateOnboardingExtensionArgs = {
channelId: transformedArgument.channelId,
userId: transformedArgument.userIdObj?.value,
numberOfDays: Number(transformedArgument.numberOfDaysObj.value),
reason: transformedArgument.reasonObj.value,
discordId: discordId,
};

const initialResponse = `<@${discordId}> Processing your request for onboarding extension`;

ctx.waitUntil(createOnboardingExtension(args, env));

return discordTextResponse(initialResponse);
}
2 changes: 2 additions & 0 deletions src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
REMOVE,
GROUP_INVITE,
GRANT_AWS_ACCESS,
ONBOARDING_EXTENSION,
} from "./constants/commands";
import { config } from "dotenv";
import { DISCORD_BASE_URL } from "./constants/urls";
Expand Down Expand Up @@ -44,6 +45,7 @@ async function registerGuildCommands(
REMOVE,
GROUP_INVITE,
GRANT_AWS_ACCESS,
ONBOARDING_EXTENSION,
];

try {
Expand Down
1 change: 1 addition & 0 deletions src/typeDefinitions/rdsUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type UserType = {
archived: boolean;
in_discord: boolean;
member?: boolean;
super_user?: boolean;
};
created_at?: number;
yoe?: number;
Expand Down
70 changes: 70 additions & 0 deletions src/utils/onboardingExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import config from "../../config/config";
import { UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST } from "../constants/responses";
import { DISCORD_BASE_URL } from "../constants/urls";
import { env } from "../typeDefinitions/default.types";
import { generateDiscordAuthToken } from "./authTokenGenerator";
import { getUserDetails } from "./getUserDetails";
import { sendReplyInDiscordChannel } from "./sendReplyInDiscordChannel";

export type CreateOnboardingExtensionArgs = {
userId?: string;
channelId: number;
reason: string;
numberOfDays: number;
discordId: string;
};

export const createOnboardingExtension = async (
args: CreateOnboardingExtensionArgs,
env: env
) => {
const { channelId } = args;

const authToken = await generateDiscordAuthToken(
"Cloudflare Worker",
Math.floor(Date.now() / 1000) + 2,
env.BOT_PRIVATE_KEY,
"RS256"
);

let content: string;
const discordReplyUrl = `${DISCORD_BASE_URL}/channels/${channelId}/messages`;

if (args.userId && args.discordId !== args.userId) {
const userResponse = await getUserDetails(args.discordId);
if (!userResponse?.user?.roles?.super_user) {
content = `<@${args.discordId}> ${UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST}`;
return await sendReplyInDiscordChannel(discordReplyUrl, content, env);
}
}

const userDiscordId = args.userId ? args.userId : args.discordId;
const base_url = config(env).RDS_BASE_API_URL;
const createOnboardingExtensionUrl = `${base_url}/requests?dev=true`;

const requestBody = {
userId: userDiscordId,
type: "ONBOARDING",
numberOfDays: args.numberOfDays,
reason: args.reason,
};

try {
const response = await fetch(createOnboardingExtensionUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify(requestBody),
});
const jsonResponse = (await response.json()) as unknown as {
message: string;
};
content = `<@${args.discordId}> ${jsonResponse.message}`;
return await sendReplyInDiscordChannel(discordReplyUrl, content, env);
} catch (err) {
content = `<@${args.discordId}> Error occurred while creating onboarding extension request.`;
return await sendReplyInDiscordChannel(discordReplyUrl, content, env);
}
};
18 changes: 18 additions & 0 deletions src/utils/sendReplyInDiscordChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { env } from "../typeDefinitions/default.types";

export const sendReplyInDiscordChannel = async (
discordReplyUrl: string,
body: any,
env: env
) => {
await fetch(discordReplyUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${env.DISCORD_TOKEN}`,
},
body: JSON.stringify({
content: body,
}),
});
};
18 changes: 18 additions & 0 deletions tests/fixtures/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,21 @@ export const testDataWithDevTitle = {
value: true,
},
};

export const transformedArgsForOnboardingExtension = {
memberObj: {
user: {
id: 134672111,
username: "username",
discriminator: "<discriminator>",
avatar: "<avatar>",
},
nick: "<nick>",
joined_at: "<joined_at>",
},
userIdObj: { name: "userId", type: 6, value: "1545562672", options: [] },
numberOfDaysObj: { value: "20", name: "numberOfDays", type: 3, options: [] },
reasonObj: { value: "reason", name: "reason", type: 3, options: [] },
channelId: 6754321,
devObj: { value: false, name: "dev", type: 5 },
};
71 changes: 71 additions & 0 deletions tests/unit/handlers/onboardingExtension.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { onboardingExtensionCommand } from "../../../src/controllers/onboardingExtensionCommand";
import { discordTextResponse } from "../../../src/utils/discordResponse";
import {
ctx,
guildEnv,
transformedArgsForOnboardingExtension,
} from "../../fixtures/fixture";
import * as utils from "../../../src/utils/onboardingExtension";

describe("onboardingExtensionCommand", () => {
beforeEach(() => {
jest.clearAllMocks();
});
afterEach(() => {
jest.restoreAllMocks();
});
const discordId =
transformedArgsForOnboardingExtension.memberObj.user.id.toString();

it("should return Feature not implemented", async () => {
const expectedRes = await onboardingExtensionCommand(
transformedArgsForOnboardingExtension,
guildEnv,
ctx
);
const jsonResponse = await expectedRes.json();
const mockResponse = discordTextResponse(
`<@${discordId}> Feature not implemented`
);
const mockJsonResponse = await mockResponse.json();
expect(jsonResponse).toStrictEqual(mockJsonResponse);
});

it("should return initial response", async () => {
transformedArgsForOnboardingExtension.devObj.value = true;
const expectedRes = await onboardingExtensionCommand(
transformedArgsForOnboardingExtension,
guildEnv,
ctx
);
const jsonResponse = await expectedRes.json();
const mockResponse = discordTextResponse(
`<@${discordId}> Processing your request for onboarding extension`
);
const mockJsonResponse = await mockResponse.json();
expect(jsonResponse).toStrictEqual(mockJsonResponse);
});

it("should call createOnboardingExtension", async () => {
jest.spyOn(utils, "createOnboardingExtension");
transformedArgsForOnboardingExtension.devObj.value = true;
await onboardingExtensionCommand(
transformedArgsForOnboardingExtension,
guildEnv,
ctx
);
expect(utils.createOnboardingExtension).toHaveBeenCalledTimes(1);
expect(utils.createOnboardingExtension).toHaveBeenCalledWith(
{
channelId: transformedArgsForOnboardingExtension.channelId,
userId: transformedArgsForOnboardingExtension.userIdObj?.value,
numberOfDays: Number(
transformedArgsForOnboardingExtension.numberOfDaysObj.value
),
reason: transformedArgsForOnboardingExtension.reasonObj.value,
discordId: discordId,
},
guildEnv
);
});
});
Loading
Loading