Skip to content

Commit c4bcef6

Browse files
authored
feat: add Discord role management functionality (#315)
* feat: add Discord role management for invite generation * feat: restructure Discord role management for environment-specific configurations
1 parent ceedefc commit c4bcef6

File tree

9 files changed

+198
-0
lines changed

9 files changed

+198
-0
lines changed

config/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
DISCORD_PROFILE_SERVICE_DEVELOPMENT_HELP_GROUP,
2424
} from "../src/constants/variables";
2525
import { config as configFromDotEnv } from "dotenv";
26+
import { DISCORD_ROLE_IDS } from "../src/constants/discordRoles";
2627

2728
export function loadEnv(env: env, fromWorkerEnv: boolean): env {
2829
const Env: env = {
@@ -78,6 +79,7 @@ const config = (env: env) => {
7879
RDS_STATUS_SITE_URL: RDS_STATUS_SITE_URL,
7980
DASHBOARD_SITE_URL: RDS_DASHBOARD_SITE_URL,
8081
MAIN_SITE_URL: RDS_MAIN_SITE_URL,
82+
DISCORD_ROLE_IDS: DISCORD_ROLE_IDS.PRODUCTION,
8183
},
8284
staging: {
8385
RDS_BASE_API_URL: RDS_BASE_STAGING_API_URL,
@@ -87,6 +89,7 @@ const config = (env: env) => {
8789
RDS_STATUS_SITE_URL: RDS_STAGING_STATUS_SITE_URL,
8890
DASHBOARD_SITE_URL: RDS_STAGING_DASHBOARD_SITE_URL,
8991
MAIN_SITE_URL: RDS_STAGING_MAIN_SITE_URL,
92+
DISCORD_ROLE_IDS: DISCORD_ROLE_IDS.STAGING,
9093
},
9194
default: {
9295
RDS_BASE_API_URL: RDS_BASE_DEVELOPMENT_API_URL,
@@ -97,6 +100,7 @@ const config = (env: env) => {
97100
RDS_STATUS_SITE_URL: RDS_STATUS_SITE_URL,
98101
DASHBOARD_SITE_URL: RDS_DASHBOARD_SITE_URL,
99102
MAIN_SITE_URL: RDS_DEVELOPMENT_MAIN_SITE_URL,
103+
DISCORD_ROLE_IDS: DISCORD_ROLE_IDS.DEVELOPMENT,
100104
},
101105
};
102106

src/constants/discordRoles.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export const DISCORD_ROLE_IDS = {
2+
STAGING: {
3+
DEVELOPER: "1121445071213056071",
4+
DESIGNER: "1471575340714430678",
5+
PRODUCT_MANAGER: "1471575685951656089",
6+
PROJECT_MANAGER: "1471575735385850018",
7+
QA: "1471575044806148331",
8+
SOCIAL_MEDIA: "1471575596554522848",
9+
UNVERIFIED: "1120875993771544687",
10+
NEW: "963882131581464697",
11+
},
12+
PRODUCTION: {
13+
DEVELOPER: "915490782939582485",
14+
DESIGNER: "1472512698628571188",
15+
PRODUCT_MANAGER: "1472512895295160475",
16+
PROJECT_MANAGER: "1472512930582106244",
17+
QA: "1472512746464743485",
18+
SOCIAL_MEDIA: "1472512852542750874",
19+
UNVERIFIED: "1103047289330745386",
20+
NEW: "1013636976647348395",
21+
},
22+
DEVELOPMENT: {
23+
DEVELOPER: "1463240239446233240",
24+
DESIGNER: "1463240507651129497",
25+
PRODUCT_MANAGER: "1468834132506448004",
26+
PROJECT_MANAGER: "1468834221929005241",
27+
QA: "1468833384813035560",
28+
SOCIAL_MEDIA: "1468833506976075999",
29+
UNVERIFIED: "1463240337458987319",
30+
NEW: "1463240348997386271",
31+
},
32+
} as const;

src/typeDefinitions/default.types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface variables {
1414
PROFILE_SERVICE_HELP_GROUP_ID: string;
1515
RDS_STATUS_SITE_URL: string;
1616
MAIN_SITE_URL: string;
17+
DISCORD_ROLE_IDS: typeof DISCORD_ROLE_IDS;
1718
}
1819

1920
export interface discordCommand {

src/typeDefinitions/discordLink.types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface inviteLinkBody {
22
channelId: string;
3+
role: string;
34
}
45

56
export interface inviteResponseType {

src/utils/generateDiscordInvite.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import config from "../../config/config";
12
import { INVITE_OPTIONS } from "../constants/inviteOptions";
23
import {
34
BAD_REQUEST,
@@ -11,18 +12,29 @@ import { DISCORD_BASE_URL } from "../constants/urls";
1112
import { env } from "../typeDefinitions/default.types";
1213
import { inviteLinkBody } from "../typeDefinitions/discordLink.types";
1314
import createDiscordHeaders from "./createDiscordHeaders";
15+
import { getInviteRoleId } from "./getInviteRoleIds";
1416

1517
export async function generateDiscordLink(
1618
body: inviteLinkBody,
1719
env: env,
1820
reason?: string
1921
) {
22+
let roleIds: string[];
23+
try {
24+
const roleIdsConfig = config(env).DISCORD_ROLE_IDS;
25+
const applicationRoleId = getInviteRoleId(body.role, env);
26+
roleIds = [applicationRoleId, roleIdsConfig.UNVERIFIED, roleIdsConfig.NEW];
27+
} catch {
28+
return BAD_REQUEST;
29+
}
30+
2031
const { channelId } = body;
2132
const generateInviteUrl = `${DISCORD_BASE_URL}/channels/${channelId}/invites`;
2233

2334
const inviteOptions = {
2435
max_uses: INVITE_OPTIONS.MAX_USE, // Maximum number of times the invite can be used
2536
unique: INVITE_OPTIONS.UNIQUE, // Whether to create a unique invite or not
37+
role_ids: roleIds,
2638
};
2739
try {
2840
const headers: HeadersInit = createDiscordHeaders({

src/utils/getInviteRoleIds.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import config from "../../config/config";
2+
import type { env } from "../typeDefinitions/default.types";
3+
4+
export function getInviteRoleId(role: string, env: env): string {
5+
const normalizedRole = role?.trim().toUpperCase();
6+
if (!normalizedRole) throw new Error("Role is required");
7+
8+
const roleIds = config(env).DISCORD_ROLE_IDS[normalizedRole];
9+
if (!roleIds) throw new Error(`Invalid role: ${role}`);
10+
11+
return roleIds;
12+
}

tests/fixtures/fixture.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export const guildEnv = {
6464

6565
export const dummyInviteBody = {
6666
channelId: "1234",
67+
role: "developer",
6768
};
6869

6970
export const dummyGuildMemberDetails = {

tests/unit/utils/generateDiscordInvite.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
import { DISCORD_ROLE_IDS } from "../../../src/constants/discordRoles";
12
import { INVITE_OPTIONS } from "../../../src/constants/inviteOptions";
23
import * as response from "../../../src/constants/responses";
34
import JSONResponse from "../../../src/utils/JsonResponse";
45
import { generateDiscordLink } from "../../../src/utils/generateDiscordInvite";
56
import { dummyInviteBody, guildEnv } from "../../fixtures/fixture";
67

8+
const defaultRoleIds = [
9+
DISCORD_ROLE_IDS.DEVELOPMENT.DEVELOPER,
10+
DISCORD_ROLE_IDS.DEVELOPMENT.UNVERIFIED,
11+
DISCORD_ROLE_IDS.DEVELOPMENT.NEW,
12+
];
13+
714
describe("generate invite link", () => {
815
it("should pass the reason to discord as a X-Audit-Log-Reason header if provided", async () => {
916
jest
@@ -26,6 +33,7 @@ describe("generate invite link", () => {
2633
body: JSON.stringify({
2734
max_uses: INVITE_OPTIONS.MAX_USE,
2835
unique: INVITE_OPTIONS.UNIQUE,
36+
role_ids: defaultRoleIds,
2937
}),
3038
}
3139
);
@@ -49,6 +57,7 @@ describe("generate invite link", () => {
4957
body: JSON.stringify({
5058
max_uses: INVITE_OPTIONS.MAX_USE,
5159
unique: INVITE_OPTIONS.UNIQUE,
60+
role_ids: defaultRoleIds,
5261
}),
5362
}
5463
);
@@ -75,8 +84,56 @@ describe("generate invite link", () => {
7584
body: JSON.stringify({
7685
max_uses: INVITE_OPTIONS.MAX_USE,
7786
unique: INVITE_OPTIONS.UNIQUE,
87+
role_ids: defaultRoleIds,
7888
}),
7989
}
8090
);
8191
});
92+
93+
test("should include three role_ids when role is passed (staging)", async () => {
94+
jest
95+
.spyOn(global, "fetch")
96+
.mockImplementation(() => Promise.resolve(new JSONResponse({})));
97+
98+
await generateDiscordLink(
99+
{ ...dummyInviteBody, role: "developer" },
100+
guildEnv
101+
);
102+
103+
expect(global.fetch).toHaveBeenCalledWith(
104+
`https://discord.com/api/v10/channels/${dummyInviteBody.channelId}/invites`,
105+
{
106+
method: "POST",
107+
headers: {
108+
"Content-Type": "application/json",
109+
Authorization: `Bot ${guildEnv.DISCORD_TOKEN}`,
110+
},
111+
body: JSON.stringify({
112+
max_uses: INVITE_OPTIONS.MAX_USE,
113+
unique: INVITE_OPTIONS.UNIQUE,
114+
role_ids: [
115+
DISCORD_ROLE_IDS.DEVELOPMENT.DEVELOPER,
116+
DISCORD_ROLE_IDS.DEVELOPMENT.UNVERIFIED,
117+
DISCORD_ROLE_IDS.DEVELOPMENT.NEW,
118+
],
119+
}),
120+
}
121+
);
122+
});
123+
124+
test("should return BAD_REQUEST when role is invalid or unknown", async () => {
125+
const result = await generateDiscordLink(
126+
{ ...dummyInviteBody, role: "unknown_role" },
127+
guildEnv
128+
);
129+
expect(result).toEqual(response.BAD_REQUEST);
130+
});
131+
132+
test("should return BAD_REQUEST when role is empty", async () => {
133+
const result = await generateDiscordLink(
134+
{ ...dummyInviteBody, role: "" },
135+
guildEnv
136+
);
137+
expect(result).toEqual(response.BAD_REQUEST);
138+
});
82139
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { DISCORD_ROLE_IDS } from "../../../src/constants/discordRoles";
2+
import { getInviteRoleId } from "../../../src/utils/getInviteRoleIds";
3+
4+
const APPLICATION_ROLE_KEYS: Record<
5+
string,
6+
keyof typeof DISCORD_ROLE_IDS.STAGING
7+
> = {
8+
developer: "DEVELOPER",
9+
designer: "DESIGNER",
10+
product_manager: "PRODUCT_MANAGER",
11+
project_manager: "PROJECT_MANAGER",
12+
qa: "QA",
13+
social_media: "SOCIAL_MEDIA",
14+
};
15+
16+
const stagingEnv = { CURRENT_ENVIRONMENT: "staging" };
17+
const productionEnv = { CURRENT_ENVIRONMENT: "production" };
18+
const defaultEnv = { CURRENT_ENVIRONMENT: "default" };
19+
20+
describe("getInviteRoleIds", () => {
21+
describe("when role is missing or invalid", () => {
22+
it("throws when role is undefined", () => {
23+
expect(() =>
24+
getInviteRoleId(undefined as unknown as string, stagingEnv)
25+
).toThrow("Role is required");
26+
});
27+
28+
it("throws when role is empty string", () => {
29+
expect(() => getInviteRoleId("", stagingEnv)).toThrow("Role is required");
30+
});
31+
32+
it("throws when role is whitespace only", () => {
33+
expect(() => getInviteRoleId(" ", stagingEnv)).toThrow(
34+
"Role is required"
35+
);
36+
});
37+
38+
it("throws when role is unknown", () => {
39+
expect(() => getInviteRoleId("unknown_role", stagingEnv)).toThrow(
40+
"Invalid role: unknown_role"
41+
);
42+
});
43+
});
44+
45+
describe("when role is valid", () => {
46+
it("returns staging role ID for valid role in staging env", () => {
47+
const result = getInviteRoleId("developer", stagingEnv);
48+
expect(result).toBe(DISCORD_ROLE_IDS.STAGING.DEVELOPER);
49+
});
50+
51+
it("returns production role ID for valid role in production env", () => {
52+
const result = getInviteRoleId("developer", productionEnv);
53+
expect(result).toBe(DISCORD_ROLE_IDS.PRODUCTION.DEVELOPER);
54+
});
55+
56+
it("returns development role ID when CURRENT_ENVIRONMENT is default", () => {
57+
const result = getInviteRoleId("designer", defaultEnv);
58+
expect(result).toBe(DISCORD_ROLE_IDS.DEVELOPMENT.DESIGNER);
59+
});
60+
61+
it("normalizes role to lowercase (case-insensitive)", () => {
62+
const result = getInviteRoleId("Developer", stagingEnv);
63+
expect(result).toBe(DISCORD_ROLE_IDS.STAGING.DEVELOPER);
64+
});
65+
66+
it("trims whitespace from role", () => {
67+
const result = getInviteRoleId(" developer ", stagingEnv);
68+
expect(result).toBe(DISCORD_ROLE_IDS.STAGING.DEVELOPER);
69+
});
70+
71+
it("returns correct ID for each of the 6 application roles (staging)", () => {
72+
for (const [role, key] of Object.entries(APPLICATION_ROLE_KEYS)) {
73+
const result = getInviteRoleId(role, stagingEnv);
74+
expect(result).toBe(DISCORD_ROLE_IDS.STAGING[key]);
75+
}
76+
});
77+
});
78+
});

0 commit comments

Comments
 (0)