diff --git a/constants/discord.ts b/constants/discord.ts index e20703f1e..917b91e42 100644 --- a/constants/discord.ts +++ b/constants/discord.ts @@ -41,6 +41,7 @@ export const endpoints = { GUILD_BANNER: (id: string, icon: string) => `${baseEndpoints.CDN_URL}/banners/${id}/${icon}`, GUILD_CHANNELS: (id: string) => `${GUILDS_BASE(id)}/channels`, + GUILD_CHANNEL: (id: string) => `${baseEndpoints.BASE_URL}/channels/${id}`, GUILD_EMBED: (id: string) => `${GUILDS_BASE(id)}/embed`, GUILD_EMOJI: (id: string, emoji_id: string) => `${GUILDS_BASE(id)}/emojis/${emoji_id}`, diff --git a/handlers/channel.ts b/handlers/channel.ts index 8a163d980..95721fff3 100644 --- a/handlers/channel.ts +++ b/handlers/channel.ts @@ -204,6 +204,85 @@ export function getChannelWebhooks(channel: Channel) { return RequestManager.get(endpoints.CHANNEL_WEBHOOKS(channel.id)); } -export function editChannel(channel: Channel, options: ChannelEditOptions) { - return RequestManager.patch(endpoints.GUILD_CHANNELS(channel.id), options); +interface EditChannelRequest { + amount: number; + timestamp: number; + channelID: string; + items: { + channel: Channel; + options: ChannelEditOptions; + }[]; +} + +const editChannelNameTopicQueue = new Map(); +let editChannelProcessing = false; + +function processEditChannelQueue() { + if (!editChannelProcessing) return; + + const now = Date.now(); + editChannelNameTopicQueue.forEach((request) => { + if (now > request.timestamp) return; + // 10 minutes have passed so we can reset this channel again + if (!request.items.length) { + return editChannelNameTopicQueue.delete(request.channelID); + } + request.amount = 0; + // There are items to process for this request + const details = request.items.shift(); + + if (!details) return; + + editChannel(details.channel, details.options); + const secondDetails = request.items.shift(); + if (!secondDetails) return; + + return editChannel(secondDetails.channel, secondDetails.options); + }); + + if (editChannelNameTopicQueue.size) { + setTimeout(() => processEditChannelQueue(), 600000); + } else { + editChannelProcessing = false; + } +} + +export function editChannel(channel: Channel, options: ChannelEditOptions) { + if (!channel.guildID) throw new Error(Errors.CHANNEL_NOT_IN_GUILD); + console.log(1); + if ( + !botHasPermission(channel.guildID, [Permissions.MANAGE_CHANNELS]) + ) { + throw new Error(Errors.MISSING_MANAGE_CHANNELS); + } + console.log(2); + if (options.name || options.topic) { + const request = editChannelNameTopicQueue.get(channel.id); + if (!request) { + // If this hasnt been done before simply add 1 for it + editChannelNameTopicQueue.set(channel.id, { + channelID: channel.id, + amount: 1, + // 10 minutes from now + timestamp: Date.now() + 600000, + items: [], + }); + } else if (request.amount === 1) { + // Start queuing future requests to this channel + request.amount = 2; + request.timestamp = Date.now() + 600000; + } else { + // 2 have already been used add to queue + request.items.push({ channel, options }); + if (editChannelProcessing) return; + editChannelProcessing = true; + processEditChannelQueue(); + return; + } + } + + return RequestManager.patch( + endpoints.GUILD_CHANNEL(channel.id), + options, + ); } diff --git a/module/requestManager.ts b/module/requestManager.ts index a0cbc7901..ce1c9672c 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -116,7 +116,12 @@ async function runMethod( retryCount = 0, bucketID?: string | null, ) { - eventHandlers.debug?.({ type: 'requestManager', data: { method, url, body, retryCount, bucketID } }); + eventHandlers.debug?.( + { + type: "requestManager", + data: { method, url, body, retryCount, bucketID }, + }, + ); return new Promise((resolve, reject) => { const callback = async () => { @@ -144,20 +149,28 @@ async function runMethod( if (retryCount > 10) { throw new Error(Errors.RATE_LIMIT_RETRY_MAXED); } - await delay(json.retry_after); - return runMethod( - method, - url, - body, - retryCount++, - bucketIDFromHeaders, + + return setTimeout( + () => + runMethod(method, url, body, retryCount++, bucketIDFromHeaders), + json.retry_after, ); } - eventHandlers.debug?.({ type: 'requestManagerSuccess', data: { method, url, body, retryCount, bucketID } }); + eventHandlers.debug?.( + { + type: "requestManagerSuccess", + data: { method, url, body, retryCount, bucketID }, + }, + ); return resolve(json); } catch (error) { - eventHandlers.debug?.({ type: 'requestManagerFailed', data: { method, url, body, retryCount, bucketID } }); + eventHandlers.debug?.( + { + type: "requestManagerFailed", + data: { method, url, body, retryCount, bucketID }, + }, + ); return reject(error); } }; @@ -175,7 +188,10 @@ async function runMethod( } function handleStatusCode(status: number) { - if (status >= 200 && status < 400) { + if ( + (status >= 200 && status < 400) || + status === HttpResponseCode.TooManyRequests + ) { return true; } @@ -185,10 +201,9 @@ function handleStatusCode(status: number) { case HttpResponseCode.Forbidden: case HttpResponseCode.NotFound: case HttpResponseCode.MethodNotAllowed: - case HttpResponseCode.TooManyRequests: - throw new Error(Errors.REQUEST_CLIENT_ERROR); + throw new Error(Errors.REQUEST_CLIENT_ERROR); case HttpResponseCode.GatewayUnavailable: - throw new Error(Errors.REQUEST_SERVER_ERROR); + throw new Error(Errors.REQUEST_SERVER_ERROR); } // left are all unknown @@ -227,7 +242,9 @@ function processHeaders(url: string, headers: Headers) { // If there is no remaining global limit, we save it in cache if (global) { const reset = Date.now() + Number(retryAfter); - eventHandlers.debug?.({ type: 'globallyRateLimited', data: { url, reset }}) + eventHandlers.debug?.( + { type: "globallyRateLimited", data: { url, reset } }, + ); globallyRateLimited = true; ratelimited = true; diff --git a/types/errors.ts b/types/errors.ts index c04a71baf..e10ff04da 100644 --- a/types/errors.ts +++ b/types/errors.ts @@ -27,4 +27,5 @@ export enum Errors { REQUEST_SERVER_ERROR = "REQUEST_SERVER_ERROR", REQUEST_UNKNOWN_ERROR = "REQUEST_UNKNOWN_ERROR", BOTS_HIGHEST_ROLE_TOO_LOW = "BOTS_HIGHEST_ROLE_TOO_LOW", + CHANNEL_NOT_IN_GUILD = "CHANNEL_NOT_IN_GUILD", }