Skip to content

Commit 937e929

Browse files
vinayak-trivedisahsisunnyprakashchoudhary07
authored
added route for generating discord invite link (RealDevSquad#130)
* added route for generating discord invite link * testing initial code * completed utils test * controller function test * tests for handler * completed test for generateDiscordInvite handlers * removed console logs * removed unused fixture items * remove unnecessary changes and types added in different file * handling different error cases * changed API method to post * typo fix --------- Co-authored-by: Sunny Sahsi <[email protected]> Co-authored-by: Prakash Choudhary <[email protected]>
1 parent 32b3cbc commit 937e929

File tree

9 files changed

+223
-2
lines changed

9 files changed

+223
-2
lines changed

src/constants/inviteOptions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const INVITE_OPTIONS = {
2+
MAX_USE: 1,
3+
UNIQUE: true,
4+
};

src/constants/responses.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export const STATUS_CHECK = {
1414
message: "Welcome to our discord Bot Server 👋",
1515
};
1616

17+
export const TOO_MANY_REQUESTS = {
18+
error: "Too many requests!",
19+
};
20+
1721
export const COMMAND_NOT_FOUND = "Command Not Found";
1822

1923
export const INTERNAL_SERVER_ERROR =
@@ -24,6 +28,8 @@ export const RETRY_COMMAND =
2428

2529
export const ROLE_ADDED = "Role added successfully";
2630

31+
export const INVITED_CREATED = "Invite created successfully!";
32+
2733
export const NAME_CHANGED = "User nickname changed successfully";
2834

2935
export const ROLE_REMOVED = "Role Removed successfully";
@@ -40,6 +46,9 @@ export const ROLE_FETCH_FAILED =
4046
export const BAD_REQUEST = {
4147
error: "Oops! This is not a proper request.",
4248
};
49+
export const UNAUTHORIZED = {
50+
error: "UnAuthorized!",
51+
};
4352

4453
export const LISTENING_SUCCESS_MESSAGE =
4554
"Your name is now changed to reflect your listening status";
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { IRequest } from "itty-router";
2+
import * as response from "../constants/responses";
3+
import { env } from "../typeDefinitions/default.types";
4+
import JSONResponse from "../utils/JsonResponse";
5+
import { verifyAuthToken } from "../utils/verifyAuthToken";
6+
import { generateDiscordLink } from "../utils/generateDiscordInvite";
7+
import { inviteLinkBody } from "../typeDefinitions/discordLink.types";
8+
9+
export async function generateInviteLink(request: IRequest, env: env) {
10+
const authHeader = request.headers.get("Authorization");
11+
if (!authHeader) {
12+
return new JSONResponse(response.BAD_SIGNATURE);
13+
}
14+
try {
15+
await verifyAuthToken(authHeader, env);
16+
const body: inviteLinkBody = await request.json();
17+
const res = await generateDiscordLink(body, env);
18+
return new JSONResponse(res);
19+
} catch (err) {
20+
return new JSONResponse(response.BAD_SIGNATURE);
21+
}
22+
}

src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { getMembersInServerHandler } from "./controllers/getMembersInServer";
1717
import { changeNickname } from "./controllers/changeNickname";
1818
import { getGuildMemberDetailsHandler } from "./controllers/getGuildMemberDetailsHandler";
1919
import { send } from "./handlers/scheduledEventHandler";
20+
import { generateInviteLink } from "./controllers/generateDiscordInvite";
2021

2122
const router = Router();
2223

@@ -44,6 +45,7 @@ router.get("/member/:id", getGuildMemberDetailsHandler);
4445
router.patch("/guild/member", changeNickname);
4546

4647
router.put("/roles/create", createGuildRoleHandler);
48+
router.post("/invite", generateInviteLink);
4749

4850
router.put("/roles/add", addGroupRoleHandler);
4951

@@ -71,10 +73,11 @@ router.all("*", async () => {
7173

7274
export default {
7375
async fetch(request: Request, env: env): Promise<Response> {
74-
if (request.method === "POST") {
76+
const apiUrls = ["/invite"];
77+
const url = new URL(request.url);
78+
if (request.method === "POST" && !apiUrls.includes(url.pathname)) {
7579
const isVerifiedRequest = await verifyBot(request, env);
7680
if (!isVerifiedRequest) {
77-
console.error("Invalid Request");
7881
return new JSONResponse(response.BAD_SIGNATURE, { status: 401 });
7982
}
8083
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface inviteLinkBody {
2+
channelId: string;
3+
}
4+
5+
export interface inviteResponseType {
6+
message: string;
7+
data: object;
8+
}

src/utils/generateDiscordInvite.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { INVITE_OPTIONS } from "../constants/inviteOptions";
2+
import {
3+
BAD_REQUEST,
4+
INTERNAL_SERVER_ERROR,
5+
INVITED_CREATED,
6+
NOT_FOUND,
7+
TOO_MANY_REQUESTS,
8+
UNAUTHORIZED,
9+
} from "../constants/responses";
10+
import { DISCORD_BASE_URL } from "../constants/urls";
11+
import { env } from "../typeDefinitions/default.types";
12+
import { inviteLinkBody } from "../typeDefinitions/discordLink.types";
13+
14+
export async function generateDiscordLink(body: inviteLinkBody, env: env) {
15+
const { channelId } = body;
16+
const generateInviteUrl = `${DISCORD_BASE_URL}/channels/${channelId}/invites`;
17+
18+
const inviteOptions = {
19+
max_uses: INVITE_OPTIONS.MAX_USE, // Maximum number of times the invite can be used
20+
unique: INVITE_OPTIONS.UNIQUE, // Whether to create a unique invite or not
21+
};
22+
try {
23+
const response = await fetch(generateInviteUrl, {
24+
method: "POST",
25+
body: JSON.stringify(inviteOptions),
26+
headers: {
27+
"Content-Type": "application/json",
28+
Authorization: `Bot ${env.DISCORD_TOKEN}`,
29+
},
30+
});
31+
32+
if (response.ok) {
33+
const data = await response.json();
34+
return { message: INVITED_CREATED, data };
35+
} else {
36+
if (response.status === 400) {
37+
return BAD_REQUEST;
38+
}
39+
40+
if (response.status === 401) {
41+
return UNAUTHORIZED;
42+
}
43+
44+
if (response.status === 404) {
45+
return NOT_FOUND;
46+
}
47+
48+
if (response.status === 429) {
49+
return TOO_MANY_REQUESTS;
50+
}
51+
52+
return INTERNAL_SERVER_ERROR;
53+
}
54+
} catch (err) {
55+
return INTERNAL_SERVER_ERROR;
56+
}
57+
}

tests/fixtures/fixture.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export const guildEnv = {
5656
DISCORD_TOKEN: "abcd",
5757
};
5858

59+
export const dummyInviteBody = {
60+
channelId: "1234",
61+
};
62+
5963
export const dummyGuildMemberDetails = {
6064
avatar: null,
6165
communication_disabled_until: null,
@@ -106,13 +110,15 @@ export const generateDummyRequestObject = ({
106110
params,
107111
query,
108112
headers, // Object of key value pair
113+
json,
109114
}: Partial<IRequest>): IRequest => {
110115
return {
111116
method: method ?? "GET",
112117
url: url ?? "/roles",
113118
params: params ?? {},
114119
query: query ?? {},
115120
headers: new Map(Object.entries(headers ?? {})),
121+
json: json,
116122
};
117123
};
118124

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { generateInviteLink } from "../../../src/controllers/generateDiscordInvite";
2+
import JSONResponse from "../../../src/utils/JsonResponse";
3+
import { generateDummyRequestObject, guildEnv } from "../../fixtures/fixture";
4+
import * as responseConstants from "../../../src/constants/responses";
5+
import { inviteResponseType } from "../../../src/typeDefinitions/discordLink.types";
6+
7+
jest.mock("../../../src/utils/verifyAuthToken", () => ({
8+
verifyAuthToken: jest.fn().mockReturnValue(true),
9+
}));
10+
11+
jest.mock("../../../src/utils/generateDiscordInvite", () => ({
12+
generateDiscordLink: jest
13+
.fn()
14+
.mockReturnValue({ data: {}, message: "Invite created successfully!" }),
15+
}));
16+
17+
describe("generate discord link", () => {
18+
it("should return 🚫 Bad Request Signature' if authtoken is there in the header", async () => {
19+
const mockRequest = generateDummyRequestObject({
20+
url: "/invite",
21+
});
22+
23+
const response: JSONResponse = await generateInviteLink(
24+
mockRequest,
25+
guildEnv
26+
);
27+
28+
const jsonResponse: { error: string } = await response.json();
29+
expect(jsonResponse).toEqual(responseConstants.BAD_SIGNATURE);
30+
});
31+
32+
it("should return data object with message on success", async () => {
33+
const mockRequest = generateDummyRequestObject({
34+
method: "PUT",
35+
url: "/invite",
36+
headers: {
37+
Authorization: "Bearer testtoken",
38+
"Content-Type": "application/json",
39+
},
40+
json: async () => {
41+
return { channelId: "xyz" };
42+
},
43+
});
44+
45+
const response: JSONResponse = await generateInviteLink(
46+
mockRequest,
47+
guildEnv
48+
);
49+
50+
const jsonResponse: inviteResponseType = await response.json();
51+
52+
expect(response.status).toBe(200);
53+
expect(jsonResponse.message).toEqual(responseConstants.INVITED_CREATED);
54+
});
55+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { INVITE_OPTIONS } from "../../../src/constants/inviteOptions";
2+
import * as response from "../../../src/constants/responses";
3+
import JSONResponse from "../../../src/utils/JsonResponse";
4+
import { generateDiscordLink } from "../../../src/utils/generateDiscordInvite";
5+
import { dummyInviteBody, guildEnv } from "../../fixtures/fixture";
6+
7+
describe("generate invite link", () => {
8+
test("should return INTERNAL_SERVER_ERROR when response is not ok", async () => {
9+
const mockResponse = new Response(null, { status: 500 });
10+
jest
11+
.spyOn(global, "fetch")
12+
.mockImplementation(() => Promise.resolve(mockResponse));
13+
14+
const result = await generateDiscordLink(dummyInviteBody, guildEnv);
15+
expect(result).toEqual(response.INTERNAL_SERVER_ERROR);
16+
expect(global.fetch).toHaveBeenCalledWith(
17+
`https://discord.com/api/v10/channels/${dummyInviteBody.channelId}/invites`,
18+
{
19+
method: "POST",
20+
headers: {
21+
"Content-Type": "application/json",
22+
Authorization: `Bot ${guildEnv.DISCORD_TOKEN}`,
23+
},
24+
body: JSON.stringify({
25+
max_uses: INVITE_OPTIONS.MAX_USE,
26+
unique: INVITE_OPTIONS.UNIQUE,
27+
}),
28+
}
29+
);
30+
});
31+
32+
test("should return JSON response when response is ok", async () => {
33+
const mockResponse = {};
34+
jest
35+
.spyOn(global, "fetch")
36+
.mockImplementation(() =>
37+
Promise.resolve(new JSONResponse(mockResponse))
38+
);
39+
40+
const result = await generateDiscordLink(dummyInviteBody, guildEnv);
41+
expect(result).toEqual({ data: {}, message: response.INVITED_CREATED });
42+
expect(global.fetch).toHaveBeenCalledWith(
43+
`https://discord.com/api/v10/channels/${dummyInviteBody.channelId}/invites`,
44+
{
45+
method: "POST",
46+
headers: {
47+
"Content-Type": "application/json",
48+
Authorization: `Bot ${guildEnv.DISCORD_TOKEN}`,
49+
},
50+
body: JSON.stringify({
51+
max_uses: INVITE_OPTIONS.MAX_USE,
52+
unique: INVITE_OPTIONS.UNIQUE,
53+
}),
54+
}
55+
);
56+
});
57+
});

0 commit comments

Comments
 (0)