Skip to content

Commit

Permalink
Merge pull request #170 from Real-Dev-Squad/develop
Browse files Browse the repository at this point in the history
Dev to main sync
  • Loading branch information
ankushdharkar authored Dec 17, 2023
2 parents d269ac9 + 19a9f80 commit ca4282d
Show file tree
Hide file tree
Showing 10 changed files with 558 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .github/workflows/register-commands-production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ jobs:
environment: production
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 18.18.2
- run: npm install
- run: npm run register
env:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/register-commands-staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ jobs:
environment: staging
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 18.18.2
- run: npm install
- run: npm run register
env:
Expand Down
3 changes: 3 additions & 0 deletions src/constants/requestsActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const GROUP_ROLE_ADD = {
ADD_ROLE: "add-role",
};
93 changes: 93 additions & 0 deletions src/controllers/guildRoleHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
memberGroupRole,
} from "../typeDefinitions/discordMessage.types";
import { verifyAuthToken } from "../utils/verifyAuthToken";
import { batchDiscordRequests } from "../utils/batchDiscordRequests";
import { DISCORD_BASE_URL } from "../constants/urls";
import { GROUP_ROLE_ADD } from "../constants/requestsActions";

export async function createGuildRoleHandler(request: IRequest, env: env) {
const authHeader = request.headers.get("Authorization");
Expand Down Expand Up @@ -46,6 +49,96 @@ export async function addGroupRoleHandler(request: IRequest, env: env) {
}
}

export async function getGuildRolesPostHandler(request: IRequest, env: env) {
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
return new JSONResponse(response.BAD_SIGNATURE);
}

try {
await verifyAuthToken(authHeader, env);
const { action } = request.query;

switch (action) {
case GROUP_ROLE_ADD.ADD_ROLE: {
const memberGroupRoleList = await request.json();
const res = await bulkAddGroupRoleHandler(memberGroupRoleList, env);
return res;
}
default: {
return new JSONResponse(response.BAD_SIGNATURE);
}
}
} catch (err) {
console.error(err);
return new JSONResponse(response.INTERNAL_SERVER_ERROR);
}
}

export async function bulkAddGroupRoleHandler(
memberGroupRoleList: memberGroupRole[],
env: env
): Promise<JSONResponse> {
try {
if (!Array.isArray(memberGroupRoleList)) {
return new JSONResponse(response.BAD_SIGNATURE, {
status: 400,
statusText: "Expecting an array for user id and role id as payload",
});
}
if (memberGroupRoleList.length < 1) {
return new JSONResponse(response.BAD_SIGNATURE, {
status: 400,
statusText: "Minimum length of request is 1",
});
}
if (memberGroupRoleList.length > 25) {
return new JSONResponse(response.BAD_SIGNATURE, {
status: 400,
statusText: "Max requests length is 25",
});
}

const addGroupRoleRequests = [];
for (const memberGroupRole of memberGroupRoleList) {
const addRoleRequest = async () => {
const { userid, roleid } = memberGroupRole;
try {
const createGuildRoleUrl = `${DISCORD_BASE_URL}/guilds/${env.DISCORD_GUILD_ID}/members/${userid}/roles/${roleid}`;
const options = {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bot ${env.DISCORD_TOKEN}`,
},
};
return await fetch(createGuildRoleUrl, options);
} catch (error) {
console.error(
`Error occurred while trying to add role: ${roleid} to user: ${userid}`,
error
);
throw error;
}
};
addGroupRoleRequests.push(addRoleRequest);
}
const responseList = await batchDiscordRequests(addGroupRoleRequests);

const responseBody = memberGroupRoleList.map((memberGroupRole, index) => {
return {
userid: memberGroupRole.userid,
roleid: memberGroupRole.roleid,
success: responseList[index].ok,
};
});
return new JSONResponse(responseBody);
} catch (e) {
console.error(e);
throw e;
}
}

export async function removeGuildRoleHandler(request: IRequest, env: env) {
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
removeGuildRoleHandler,
getGuildRoleByRoleNameHandler,
getGuildRolesHandler,
getGuildRolesPostHandler,
} from "./controllers/guildRoleHandler";
import { getMembersInServerHandler } from "./controllers/getMembersInServer";
import { changeNickname } from "./controllers/changeNickname";
Expand All @@ -33,6 +34,8 @@ router.put("/roles/create", createGuildRoleHandler);

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

router.post("/roles", getGuildRolesPostHandler);

router.delete("/roles", removeGuildRoleHandler);

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

export default {
async fetch(request: Request, env: env): Promise<Response> {
const apiUrls = ["/invite"];
const apiUrls = ["/invite", "/roles"];
const url = new URL(request.url);
if (request.method === "POST" && !apiUrls.includes(url.pathname)) {
const isVerifiedRequest = await verifyBot(request, env);
Expand Down
133 changes: 133 additions & 0 deletions src/utils/batchDiscordRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import JSONResponse from "./JsonResponse";
import { addDelay, convertSecondsToMillis } from "./timeUtils";
export const DISCORD_HEADERS = {
RATE_LIMIT_RESET_AFTER: "X-RateLimit-Reset-After",
RATE_LIMIT_REMAINING: "X-RateLimit-Remaining",
RETRY_AFTER: "Retry-After",
};

const MAX_RETRY = 1;
const LIMIT_BUFFER = 0.2;

interface RequestDetails {
retries: number;
request: () => Promise<Response>;
index: number;
}
interface ResponseDetails {
response: Response;
data: RequestDetails;
}

const parseRateLimitRemaining = (response: Response) => {
let rateLimitRemaining = Number.parseInt(
response.headers.get(DISCORD_HEADERS.RATE_LIMIT_REMAINING) || "0"
);
rateLimitRemaining = Math.floor(rateLimitRemaining * (1 - LIMIT_BUFFER));
return rateLimitRemaining;
};

const parseResetAfter = (response: Response) => {
let resetAfter = Number.parseFloat(
response.headers.get(DISCORD_HEADERS.RATE_LIMIT_RESET_AFTER) || "0"
);
resetAfter = Math.ceil(resetAfter);
return resetAfter;
};

export const batchDiscordRequests = async (
requests: { (): Promise<Response> }[]
): Promise<Response[]> => {
try {
const requestsQueue: RequestDetails[] = requests.map((request, index) => {
return {
retries: 0,
request: request,
index: index,
};
});

const responseList: Response[] = new Array(requestsQueue.length);
let resetAfter = 0;
let nextMinimumResetAfter = Infinity;
let rateLimitRemaining = 1;
let nextMinimumRateLimitRemaining = Infinity;

const handleResponse = async (
response: JSONResponse,
data: RequestDetails
): Promise<void> => {
if (response.ok) {
nextMinimumResetAfter = Math.min(
nextMinimumResetAfter,
parseResetAfter(response)
);
nextMinimumRateLimitRemaining = Math.min(
nextMinimumRateLimitRemaining,
parseRateLimitRemaining(response)
);

responseList[data.index] = response;
} else {
nextMinimumResetAfter = Math.min(
nextMinimumResetAfter,
parseResetAfter(response)
);
rateLimitRemaining = 0;
if (data.retries >= MAX_RETRY) {
responseList[data.index] = response;
} else {
data.retries++;
requestsQueue.push(data);
}
}
};

const executeRequest = async (
data: RequestDetails
): Promise<{ response: Response; data: RequestDetails }> => {
let response;
try {
response = await data.request();
} catch (e: unknown) {
console.error(`Error executing request at index ${data.index}:`, e);
response = new JSONResponse({ error: e }, { status: 500 });
}
return { response, data };
};

let promises: Promise<{ response: Response; data: RequestDetails }>[] = [];

while (requestsQueue.length > 0) {
const requestData = requestsQueue.pop();
if (!requestData) continue;
promises.push(executeRequest(requestData));
rateLimitRemaining--;

if (rateLimitRemaining <= 0 || requestsQueue.length === 0) {
const resultList: ResponseDetails[] = await Promise.all(promises);
promises = [];
for (const result of resultList) {
const { response, data } = result;
await handleResponse(response, data);
}
if (nextMinimumRateLimitRemaining !== Infinity) {
rateLimitRemaining = nextMinimumRateLimitRemaining;
}
if (nextMinimumResetAfter !== Infinity) {
resetAfter = nextMinimumResetAfter;
}
nextMinimumRateLimitRemaining = Infinity;
nextMinimumResetAfter = Infinity;
if (rateLimitRemaining <= 0 && resetAfter) {
await addDelay(convertSecondsToMillis(resetAfter));
rateLimitRemaining = 1;
}
}
}
return responseList;
} catch (e) {
console.error("Error in batchDiscordRequests:", e);
throw e;
}
};
7 changes: 7 additions & 0 deletions src/utils/timeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const addDelay = async (millisecond: number): Promise<void> => {
await new Promise<void>((resolve) => setTimeout(resolve, millisecond));
};

export const convertSecondsToMillis = (seconds: number): number => {
return Math.ceil(seconds * 1000);
};
12 changes: 12 additions & 0 deletions tests/fixtures/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,15 @@ export const userFutureStatusMock: UserStatus = {
},
message: "User Status found successfully.",
};

export const memberGroupRoleList: memberGroupRole[] = [
{ userid: "XXXX", roleid: "XXXX" },
{ userid: "YYYY", roleid: "YYYY" },
{ userid: "ZZZZ", roleid: "ZZZZ" },
];

export const memberGroupRoleResponseList = [
{ userid: "XXXX", roleid: "XXXX", success: true },
{ userid: "YYYY", roleid: "YYYY", success: true },
{ userid: "ZZZZ", roleid: "ZZZZ", success: true },
];
Loading

0 comments on commit ca4282d

Please sign in to comment.