Skip to content

Commit ca4282d

Browse files
Merge pull request #170 from Real-Dev-Squad/develop
Dev to main sync
2 parents d269ac9 + 19a9f80 commit ca4282d

File tree

10 files changed

+558
-1
lines changed

10 files changed

+558
-1
lines changed

.github/workflows/register-commands-production.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ jobs:
88
environment: production
99
steps:
1010
- uses: actions/checkout@v2
11+
- uses: actions/setup-node@v3
12+
with:
13+
node-version: 18.18.2
1114
- run: npm install
1215
- run: npm run register
1316
env:

.github/workflows/register-commands-staging.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ jobs:
88
environment: staging
99
steps:
1010
- uses: actions/checkout@v2
11+
- uses: actions/setup-node@v3
12+
with:
13+
node-version: 18.18.2
1114
- run: npm install
1215
- run: npm run register
1316
env:

src/constants/requestsActions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const GROUP_ROLE_ADD = {
2+
ADD_ROLE: "add-role",
3+
};

src/controllers/guildRoleHandler.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {
1414
memberGroupRole,
1515
} from "../typeDefinitions/discordMessage.types";
1616
import { verifyAuthToken } from "../utils/verifyAuthToken";
17+
import { batchDiscordRequests } from "../utils/batchDiscordRequests";
18+
import { DISCORD_BASE_URL } from "../constants/urls";
19+
import { GROUP_ROLE_ADD } from "../constants/requestsActions";
1720

1821
export async function createGuildRoleHandler(request: IRequest, env: env) {
1922
const authHeader = request.headers.get("Authorization");
@@ -46,6 +49,96 @@ export async function addGroupRoleHandler(request: IRequest, env: env) {
4649
}
4750
}
4851

52+
export async function getGuildRolesPostHandler(request: IRequest, env: env) {
53+
const authHeader = request.headers.get("Authorization");
54+
if (!authHeader) {
55+
return new JSONResponse(response.BAD_SIGNATURE);
56+
}
57+
58+
try {
59+
await verifyAuthToken(authHeader, env);
60+
const { action } = request.query;
61+
62+
switch (action) {
63+
case GROUP_ROLE_ADD.ADD_ROLE: {
64+
const memberGroupRoleList = await request.json();
65+
const res = await bulkAddGroupRoleHandler(memberGroupRoleList, env);
66+
return res;
67+
}
68+
default: {
69+
return new JSONResponse(response.BAD_SIGNATURE);
70+
}
71+
}
72+
} catch (err) {
73+
console.error(err);
74+
return new JSONResponse(response.INTERNAL_SERVER_ERROR);
75+
}
76+
}
77+
78+
export async function bulkAddGroupRoleHandler(
79+
memberGroupRoleList: memberGroupRole[],
80+
env: env
81+
): Promise<JSONResponse> {
82+
try {
83+
if (!Array.isArray(memberGroupRoleList)) {
84+
return new JSONResponse(response.BAD_SIGNATURE, {
85+
status: 400,
86+
statusText: "Expecting an array for user id and role id as payload",
87+
});
88+
}
89+
if (memberGroupRoleList.length < 1) {
90+
return new JSONResponse(response.BAD_SIGNATURE, {
91+
status: 400,
92+
statusText: "Minimum length of request is 1",
93+
});
94+
}
95+
if (memberGroupRoleList.length > 25) {
96+
return new JSONResponse(response.BAD_SIGNATURE, {
97+
status: 400,
98+
statusText: "Max requests length is 25",
99+
});
100+
}
101+
102+
const addGroupRoleRequests = [];
103+
for (const memberGroupRole of memberGroupRoleList) {
104+
const addRoleRequest = async () => {
105+
const { userid, roleid } = memberGroupRole;
106+
try {
107+
const createGuildRoleUrl = `${DISCORD_BASE_URL}/guilds/${env.DISCORD_GUILD_ID}/members/${userid}/roles/${roleid}`;
108+
const options = {
109+
method: "PUT",
110+
headers: {
111+
"Content-Type": "application/json",
112+
Authorization: `Bot ${env.DISCORD_TOKEN}`,
113+
},
114+
};
115+
return await fetch(createGuildRoleUrl, options);
116+
} catch (error) {
117+
console.error(
118+
`Error occurred while trying to add role: ${roleid} to user: ${userid}`,
119+
error
120+
);
121+
throw error;
122+
}
123+
};
124+
addGroupRoleRequests.push(addRoleRequest);
125+
}
126+
const responseList = await batchDiscordRequests(addGroupRoleRequests);
127+
128+
const responseBody = memberGroupRoleList.map((memberGroupRole, index) => {
129+
return {
130+
userid: memberGroupRole.userid,
131+
roleid: memberGroupRole.roleid,
132+
success: responseList[index].ok,
133+
};
134+
});
135+
return new JSONResponse(responseBody);
136+
} catch (e) {
137+
console.error(e);
138+
throw e;
139+
}
140+
}
141+
49142
export async function removeGuildRoleHandler(request: IRequest, env: env) {
50143
const authHeader = request.headers.get("Authorization");
51144
if (!authHeader) {

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
removeGuildRoleHandler,
1313
getGuildRoleByRoleNameHandler,
1414
getGuildRolesHandler,
15+
getGuildRolesPostHandler,
1516
} from "./controllers/guildRoleHandler";
1617
import { getMembersInServerHandler } from "./controllers/getMembersInServer";
1718
import { changeNickname } from "./controllers/changeNickname";
@@ -33,6 +34,8 @@ router.put("/roles/create", createGuildRoleHandler);
3334

3435
router.put("/roles/add", addGroupRoleHandler);
3536

37+
router.post("/roles", getGuildRolesPostHandler);
38+
3639
router.delete("/roles", removeGuildRoleHandler);
3740

3841
router.get("/roles", getGuildRolesHandler);
@@ -73,7 +76,7 @@ router.all("*", async () => {
7376

7477
export default {
7578
async fetch(request: Request, env: env): Promise<Response> {
76-
const apiUrls = ["/invite"];
79+
const apiUrls = ["/invite", "/roles"];
7780
const url = new URL(request.url);
7881
if (request.method === "POST" && !apiUrls.includes(url.pathname)) {
7982
const isVerifiedRequest = await verifyBot(request, env);

src/utils/batchDiscordRequests.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import JSONResponse from "./JsonResponse";
2+
import { addDelay, convertSecondsToMillis } from "./timeUtils";
3+
export const DISCORD_HEADERS = {
4+
RATE_LIMIT_RESET_AFTER: "X-RateLimit-Reset-After",
5+
RATE_LIMIT_REMAINING: "X-RateLimit-Remaining",
6+
RETRY_AFTER: "Retry-After",
7+
};
8+
9+
const MAX_RETRY = 1;
10+
const LIMIT_BUFFER = 0.2;
11+
12+
interface RequestDetails {
13+
retries: number;
14+
request: () => Promise<Response>;
15+
index: number;
16+
}
17+
interface ResponseDetails {
18+
response: Response;
19+
data: RequestDetails;
20+
}
21+
22+
const parseRateLimitRemaining = (response: Response) => {
23+
let rateLimitRemaining = Number.parseInt(
24+
response.headers.get(DISCORD_HEADERS.RATE_LIMIT_REMAINING) || "0"
25+
);
26+
rateLimitRemaining = Math.floor(rateLimitRemaining * (1 - LIMIT_BUFFER));
27+
return rateLimitRemaining;
28+
};
29+
30+
const parseResetAfter = (response: Response) => {
31+
let resetAfter = Number.parseFloat(
32+
response.headers.get(DISCORD_HEADERS.RATE_LIMIT_RESET_AFTER) || "0"
33+
);
34+
resetAfter = Math.ceil(resetAfter);
35+
return resetAfter;
36+
};
37+
38+
export const batchDiscordRequests = async (
39+
requests: { (): Promise<Response> }[]
40+
): Promise<Response[]> => {
41+
try {
42+
const requestsQueue: RequestDetails[] = requests.map((request, index) => {
43+
return {
44+
retries: 0,
45+
request: request,
46+
index: index,
47+
};
48+
});
49+
50+
const responseList: Response[] = new Array(requestsQueue.length);
51+
let resetAfter = 0;
52+
let nextMinimumResetAfter = Infinity;
53+
let rateLimitRemaining = 1;
54+
let nextMinimumRateLimitRemaining = Infinity;
55+
56+
const handleResponse = async (
57+
response: JSONResponse,
58+
data: RequestDetails
59+
): Promise<void> => {
60+
if (response.ok) {
61+
nextMinimumResetAfter = Math.min(
62+
nextMinimumResetAfter,
63+
parseResetAfter(response)
64+
);
65+
nextMinimumRateLimitRemaining = Math.min(
66+
nextMinimumRateLimitRemaining,
67+
parseRateLimitRemaining(response)
68+
);
69+
70+
responseList[data.index] = response;
71+
} else {
72+
nextMinimumResetAfter = Math.min(
73+
nextMinimumResetAfter,
74+
parseResetAfter(response)
75+
);
76+
rateLimitRemaining = 0;
77+
if (data.retries >= MAX_RETRY) {
78+
responseList[data.index] = response;
79+
} else {
80+
data.retries++;
81+
requestsQueue.push(data);
82+
}
83+
}
84+
};
85+
86+
const executeRequest = async (
87+
data: RequestDetails
88+
): Promise<{ response: Response; data: RequestDetails }> => {
89+
let response;
90+
try {
91+
response = await data.request();
92+
} catch (e: unknown) {
93+
console.error(`Error executing request at index ${data.index}:`, e);
94+
response = new JSONResponse({ error: e }, { status: 500 });
95+
}
96+
return { response, data };
97+
};
98+
99+
let promises: Promise<{ response: Response; data: RequestDetails }>[] = [];
100+
101+
while (requestsQueue.length > 0) {
102+
const requestData = requestsQueue.pop();
103+
if (!requestData) continue;
104+
promises.push(executeRequest(requestData));
105+
rateLimitRemaining--;
106+
107+
if (rateLimitRemaining <= 0 || requestsQueue.length === 0) {
108+
const resultList: ResponseDetails[] = await Promise.all(promises);
109+
promises = [];
110+
for (const result of resultList) {
111+
const { response, data } = result;
112+
await handleResponse(response, data);
113+
}
114+
if (nextMinimumRateLimitRemaining !== Infinity) {
115+
rateLimitRemaining = nextMinimumRateLimitRemaining;
116+
}
117+
if (nextMinimumResetAfter !== Infinity) {
118+
resetAfter = nextMinimumResetAfter;
119+
}
120+
nextMinimumRateLimitRemaining = Infinity;
121+
nextMinimumResetAfter = Infinity;
122+
if (rateLimitRemaining <= 0 && resetAfter) {
123+
await addDelay(convertSecondsToMillis(resetAfter));
124+
rateLimitRemaining = 1;
125+
}
126+
}
127+
}
128+
return responseList;
129+
} catch (e) {
130+
console.error("Error in batchDiscordRequests:", e);
131+
throw e;
132+
}
133+
};

src/utils/timeUtils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const addDelay = async (millisecond: number): Promise<void> => {
2+
await new Promise<void>((resolve) => setTimeout(resolve, millisecond));
3+
};
4+
5+
export const convertSecondsToMillis = (seconds: number): number => {
6+
return Math.ceil(seconds * 1000);
7+
};

tests/fixtures/fixture.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,15 @@ export const userFutureStatusMock: UserStatus = {
248248
},
249249
message: "User Status found successfully.",
250250
};
251+
252+
export const memberGroupRoleList: memberGroupRole[] = [
253+
{ userid: "XXXX", roleid: "XXXX" },
254+
{ userid: "YYYY", roleid: "YYYY" },
255+
{ userid: "ZZZZ", roleid: "ZZZZ" },
256+
];
257+
258+
export const memberGroupRoleResponseList = [
259+
{ userid: "XXXX", roleid: "XXXX", success: true },
260+
{ userid: "YYYY", roleid: "YYYY", success: true },
261+
{ userid: "ZZZZ", roleid: "ZZZZ", success: true },
262+
];

0 commit comments

Comments
 (0)