Added GuildEmojisManager, Guild#emojis, and refactor RESTManager
This commit is contained in:
		
							parent
							
								
									4796252798
								
							
						
					
					
						commit
						c2e690fe78
					
				
					 8 changed files with 343 additions and 237 deletions
				
			
		|  | @ -10,6 +10,17 @@ export class EmojisManager extends BaseManager<EmojiPayload, Emoji> { | |||
|     super(client, `emojis`, Emoji) | ||||
|   } | ||||
| 
 | ||||
|   async get (key: string): Promise<Emoji | undefined> { | ||||
|     const raw = await this._get(key) | ||||
|     if (raw === undefined) return | ||||
|     const emoji = new this.DataType(this.client, raw) | ||||
|     if((raw as any).guild_id !== undefined) { | ||||
|       const guild = await this.client.guilds.get((raw as any).guild_id) | ||||
|       if(guild !== undefined) emoji.guild = guild | ||||
|     } | ||||
|     return emoji | ||||
|   } | ||||
| 
 | ||||
|   async fetch (guildID: string, id: string): Promise<Emoji> { | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       this.client.rest | ||||
|  |  | |||
							
								
								
									
										91
									
								
								src/managers/guildEmojis.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/managers/guildEmojis.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,91 @@ | |||
| import { Client } from '../models/client.ts' | ||||
| import { Emoji } from "../structures/emoji.ts" | ||||
| import { Guild } from '../structures/guild.ts' | ||||
| import { Role } from "../structures/role.ts" | ||||
| import { EmojiPayload } from "../types/emoji.ts" | ||||
| import { CHANNEL, GUILD_EMOJI, GUILD_EMOJIS } from '../types/endpoint.ts' | ||||
| import { BaseChildManager } from './baseChild.ts' | ||||
| import { EmojisManager } from "./emojis.ts" | ||||
| import { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts' | ||||
| 
 | ||||
| export class GuildEmojisManager extends BaseChildManager< | ||||
|   EmojiPayload, | ||||
|   Emoji | ||||
|   > { | ||||
|   guild: Guild | ||||
| 
 | ||||
|   constructor(client: Client, parent: EmojisManager, guild: Guild) { | ||||
|     super(client, parent as any) | ||||
|     this.guild = guild | ||||
|   } | ||||
| 
 | ||||
|   async get(id: string): Promise<Emoji | undefined> { | ||||
|     const res = await this.parent.get(id) | ||||
|     if (res !== undefined && res.guild?.id === this.guild.id) return res | ||||
|     else return undefined | ||||
|   } | ||||
| 
 | ||||
|   async delete(id: string): Promise<boolean> { | ||||
|     return this.client.rest.delete(CHANNEL(id)) | ||||
|   } | ||||
| 
 | ||||
|   async fetch(id: string): Promise<Emoji | undefined> { | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       this.client.rest | ||||
|         .get(GUILD_EMOJI(this.guild.id, id)) | ||||
|         .then(async data => { | ||||
|           const emoji = new Emoji(this.client, data as EmojiPayload) | ||||
|           data.guild_id = this.guild.id | ||||
|           await this.set(id, data as EmojiPayload) | ||||
|           emoji.guild = this.guild | ||||
|           resolve(emoji) | ||||
|         }) | ||||
|         .catch(e => reject(e)) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async create(name: string, url: string, roles?: Role[] | string[] | string): Promise<Emoji | undefined> { | ||||
|     let data = url | ||||
|     if (!data.startsWith("data:")) { | ||||
|       data = await fetchAuto(url) | ||||
|     } | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       let roleIDs: string[] = [] | ||||
|       if (roles !== undefined && typeof roles === "string") roleIDs = [roles] | ||||
|       else if (roles !== undefined) { | ||||
|         if(roles?.length === 0) reject(new Error("Empty Roles array was provided")) | ||||
|         if(roles[0] instanceof Role) roleIDs = (roles as any).map((r: Role) => r.id) | ||||
|         else roleIDs = roles as string[] | ||||
|       } else roles = [this.guild.id] | ||||
|       this.client.rest | ||||
|         .post(GUILD_EMOJIS(this.guild.id), { | ||||
|           name, | ||||
|           image: data, | ||||
|           roles: roleIDs | ||||
|         }) | ||||
|         .then(async data => { | ||||
|           const emoji = new Emoji(this.client, data as EmojiPayload) | ||||
|           data.guild_id = this.guild.id | ||||
|           await this.set(data.id, data as EmojiPayload) | ||||
|           emoji.guild = this.guild | ||||
|           resolve(emoji) | ||||
|         }) | ||||
|         .catch(e => reject(e)) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async array(): Promise<Emoji[]> { | ||||
|     const arr = (await this.parent.array()) as Emoji[] | ||||
|     return arr.filter( | ||||
|       (c: any) => c.guild !== undefined && c.guild.id === this.guild.id | ||||
|     ) as any | ||||
|   } | ||||
| 
 | ||||
|   async flush(): Promise<boolean> { | ||||
|     const arr = await this.array() | ||||
|     for (const elem of arr) { | ||||
|       this.parent.delete(elem.id) | ||||
|     } | ||||
|     return true | ||||
|   } | ||||
| } | ||||
|  | @ -2,6 +2,9 @@ import { delay } from '../utils/index.ts' | |||
| import * as baseEndpoints from '../consts/urlsAndVersions.ts' | ||||
| import { Client } from './client.ts' | ||||
| import { getBuildInfo } from '../utils/buildInfo.ts' | ||||
| import { Collection } from "../utils/collection.ts" | ||||
| 
 | ||||
| export type RequestMethods = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' | ||||
| 
 | ||||
| export enum HttpResponseCode { | ||||
|   Ok = 200, | ||||
|  | @ -17,122 +20,102 @@ export enum HttpResponseCode { | |||
|   GatewayUnavailable = 502 | ||||
| } | ||||
| 
 | ||||
| export type RequestMethods = | ||||
|   | 'get' | ||||
|   | 'post' | ||||
|   | 'put' | ||||
|   | 'patch' | ||||
|   | 'head' | ||||
|   | 'delete' | ||||
| export interface RequestHeaders { | ||||
|   [name: string]: string | ||||
| } | ||||
| 
 | ||||
| export interface QueuedRequest { | ||||
|   callback: () => Promise< | ||||
|     | { | ||||
|         rateLimited: any | ||||
|         beforeFetch: boolean | ||||
|         bucketID?: string | null | ||||
|       } | ||||
|     | undefined | ||||
|   > | ||||
|   bucketID?: string | null | ||||
| export interface QueuedItem { | ||||
|   onComplete: () => Promise<{ | ||||
|     rateLimited: any | ||||
|     bucket?: string | null | ||||
|     before: boolean | ||||
|   } | undefined> | ||||
|   bucket?: string | null | ||||
|   url: string | ||||
| } | ||||
| 
 | ||||
| export interface RateLimitedPath { | ||||
| export interface RateLimit { | ||||
|   url: string | ||||
|   resetTimestamp: number | ||||
|   bucketID: string | null | ||||
|   resetAt: number | ||||
|   bucket: string | null | ||||
| } | ||||
| 
 | ||||
| export class RESTManager { | ||||
|   client: Client | ||||
|   globallyRateLimited: boolean = false | ||||
|   queueInProcess: boolean = false | ||||
|   pathQueues: { [key: string]: QueuedRequest[] } = {} | ||||
|   ratelimitedPaths = new Map<string, RateLimitedPath>() | ||||
|   queues: { [key: string]: QueuedItem[] } = {} | ||||
|   rateLimits = new Collection<string, RateLimit>() | ||||
|   globalRateLimit: boolean = false | ||||
|   processing: boolean = false | ||||
| 
 | ||||
|   constructor (client: Client) { | ||||
|   constructor(client: Client) { | ||||
|     this.client = client | ||||
|     setTimeout(() => this.processRateLimitedPaths, 1000) | ||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||
|     this.handleRateLimits() | ||||
|   } | ||||
| 
 | ||||
|   async processRateLimitedPaths (): Promise<void> { | ||||
|     const now = Date.now() | ||||
|     this.ratelimitedPaths.forEach((value, key) => { | ||||
|       if (value.resetTimestamp > now) return | ||||
|       this.ratelimitedPaths.delete(key) | ||||
|       if (key === 'global') this.globallyRateLimited = false | ||||
|   async checkQueues(): Promise<void> { | ||||
|     Object.entries(this.queues).forEach(([key, value]) => { | ||||
|       if (value.length === 0) { | ||||
|         // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
 | ||||
|         delete this.queues[key] | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   addToQueue (request: QueuedRequest): void { | ||||
|   queue(request: QueuedItem): void { | ||||
|     const route = request.url.substring( | ||||
|       // eslint seriously?
 | ||||
|       // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
 | ||||
|       baseEndpoints.DISCORD_API_URL.length + 1 | ||||
|     ) | ||||
|     const parts = route.split('/') | ||||
|     // Remove the major param
 | ||||
|     parts.shift() | ||||
|     const [id] = parts | ||||
| 
 | ||||
|     if (this.pathQueues[id] !== undefined) { | ||||
|       this.pathQueues[id].push(request) | ||||
|     if (this.queues[id] !== undefined) { | ||||
|       this.queues[id].push(request) | ||||
|     } else { | ||||
|       this.pathQueues[id] = [request] | ||||
|       this.queues[id] = [request] | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async cleanupQueues (): Promise<void> { | ||||
|     Object.entries(this.pathQueues).forEach(([key, value]) => { | ||||
|       if (value.length === 0) { | ||||
|         // Remove it entirely
 | ||||
|         // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
 | ||||
|         delete this.pathQueues[key] | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async processQueue (): Promise<void> { | ||||
|     if ( | ||||
|       Object.keys(this.pathQueues).length !== 0 && | ||||
|       !this.globallyRateLimited | ||||
|     ) { | ||||
|   async processQueue(): Promise<void> { | ||||
|     if (Object.keys(this.queues).length !== 0 && !this.globalRateLimit) { | ||||
|       await Promise.allSettled( | ||||
|         Object.values(this.pathQueues).map(async pathQueue => { | ||||
|         Object.values(this.queues).map(async pathQueue => { | ||||
|           const request = pathQueue.shift() | ||||
|           if (request === undefined) return | ||||
| 
 | ||||
|           const rateLimitedURLResetIn = await this.checkRatelimits(request.url) | ||||
|           const rateLimitedURLResetIn = await this.isRateLimited(request.url) | ||||
| 
 | ||||
|           if (typeof request.bucketID === 'string') { | ||||
|             const rateLimitResetIn = await this.checkRatelimits( | ||||
|               request.bucketID | ||||
|           if (typeof request.bucket === 'string') { | ||||
|             const rateLimitResetIn = await this.isRateLimited( | ||||
|               request.bucket | ||||
|             ) | ||||
|             if (rateLimitResetIn !== false) { | ||||
|               // This request is still rate limited read to queue
 | ||||
|               this.addToQueue(request) | ||||
|               this.queue(request) | ||||
|             } else { | ||||
|               // This request is not rate limited so it should be run
 | ||||
|               const result = await request.callback() | ||||
|               const result = await request.onComplete() | ||||
|               if (result?.rateLimited !== undefined) { | ||||
|                 this.addToQueue({ | ||||
|                 this.queue({ | ||||
|                   ...request, | ||||
|                   bucketID: result.bucketID ?? request.bucketID | ||||
|                   bucket: result.bucket ?? request.bucket | ||||
|                 }) | ||||
|               } | ||||
|             } | ||||
|           } else { | ||||
|             if (rateLimitedURLResetIn !== false) { | ||||
|               // This URL is rate limited readd to queue
 | ||||
|               this.addToQueue(request) | ||||
|               this.queue(request) | ||||
|             } else { | ||||
|               // This request has no bucket id so it should be processed
 | ||||
|               const result = await request.callback() | ||||
|               const result = await request.onComplete() | ||||
|               if (result?.rateLimited !== undefined) { | ||||
|                 this.addToQueue({ | ||||
|                 this.queue({ | ||||
|                   ...request, | ||||
|                   bucketID: result.bucketID ?? request.bucketID | ||||
|                   bucket: result.bucket ?? request.bucket | ||||
|                 }) | ||||
|               } | ||||
|             } | ||||
|  | @ -141,27 +124,28 @@ export class RESTManager { | |||
|       ) | ||||
|     } | ||||
| 
 | ||||
|     if (Object.keys(this.pathQueues).length !== 0) { | ||||
|     if (Object.keys(this.queues).length !== 0) { | ||||
|       await delay(1000) | ||||
|       // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||
|       this.processQueue() | ||||
|       // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||
|       this.cleanupQueues() | ||||
|     } else this.queueInProcess = false | ||||
|       this.checkQueues() | ||||
|     } else this.processing = false | ||||
|   } | ||||
| 
 | ||||
|   createRequestBody ( | ||||
|   prepare( | ||||
|     body: any, | ||||
|     method: RequestMethods | ||||
|   ): { [key: string]: any } { | ||||
|     const headers: { [key: string]: string } = { | ||||
|       Authorization: `Bot ${this.client.token}`, | ||||
|       'User-Agent': `DiscordBot (harmony)` | ||||
| 
 | ||||
|     const headers: RequestHeaders = { | ||||
|       'Authorization': `Bot ${this.client.token}`, | ||||
|       'User-Agent': `DiscordBot (harmony, https://github.com/harmony-org/harmony)` | ||||
|     } | ||||
| 
 | ||||
|     if (this.client.token === undefined) delete headers.Authorization | ||||
| 
 | ||||
|     if (method === 'get') body = undefined | ||||
|     if (method === 'get' || method === 'head' || method === 'delete') body = undefined | ||||
| 
 | ||||
|     if (body?.reason !== undefined) { | ||||
|       headers['X-Audit-Log-Reason'] = encodeURIComponent(body.reason) | ||||
|  | @ -203,117 +187,73 @@ export class RESTManager { | |||
|     return data | ||||
|   } | ||||
| 
 | ||||
|   async checkRatelimits (url: string): Promise<number | false> { | ||||
|     const ratelimited = this.ratelimitedPaths.get(url) | ||||
|     const global = this.ratelimitedPaths.get('global') | ||||
|   async isRateLimited(url: string): Promise<number | false> { | ||||
|     const global = this.rateLimits.get('global') | ||||
|     const rateLimited = this.rateLimits.get(url) | ||||
|     const now = Date.now() | ||||
| 
 | ||||
|     if (ratelimited !== undefined && now < ratelimited.resetTimestamp) { | ||||
|       return ratelimited.resetTimestamp - now | ||||
|     if (rateLimited !== undefined && now < rateLimited.resetAt) { | ||||
|       return rateLimited.resetAt - now | ||||
|     } | ||||
|     if (global !== undefined && now < global.resetTimestamp) { | ||||
|       return global.resetTimestamp - now | ||||
|     if (global !== undefined && now < global.resetAt) { | ||||
|       return global.resetAt - now | ||||
|     } | ||||
| 
 | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   async runMethod ( | ||||
|     method: RequestMethods, | ||||
|     url: string, | ||||
|     body?: unknown, | ||||
|     retryCount = 0, | ||||
|     bucketID?: string | null | ||||
|   ): Promise<any> { | ||||
|     const errorStack = new Error('Location In Your Files:') | ||||
|     Error.captureStackTrace(errorStack) | ||||
|   processHeaders(url: string, headers: Headers): string | null | undefined { | ||||
|     let rateLimited = false | ||||
| 
 | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       const callback = async (): Promise<undefined | any> => { | ||||
|         try { | ||||
|           const rateLimitResetIn = await this.checkRatelimits(url) | ||||
|           if (rateLimitResetIn !== false) { | ||||
|             return { | ||||
|               rateLimited: rateLimitResetIn, | ||||
|               beforeFetch: true, | ||||
|               bucketID | ||||
|             } | ||||
|           } | ||||
|     const global = headers.get('x-ratelimit-global') | ||||
|     const bucket = headers.get('x-ratelimit-bucket') | ||||
|     const remaining = headers.get('x-ratelimit-remaining') | ||||
|     const resetAt = headers.get('x-ratelimit-reset') | ||||
|     const retryAfter = headers.get('retry-after') | ||||
| 
 | ||||
|           const query = | ||||
|             method === 'get' && body !== undefined | ||||
|               ? Object.entries(body as any) | ||||
|                   .map( | ||||
|                     ([key, value]) => | ||||
|                       `${encodeURIComponent(key)}=${encodeURIComponent( | ||||
|                         value as any | ||||
|                       )}` | ||||
|                   ) | ||||
|                   .join('&') | ||||
|               : '' | ||||
|           let urlToUse = | ||||
|             method === 'get' && query !== '' ? `${url}?${query}` : url | ||||
|     if (remaining !== null && remaining === '0') { | ||||
|       rateLimited = true | ||||
| 
 | ||||
|           if (this.client.canary === true) { | ||||
|             const split = urlToUse.split('//') | ||||
|             urlToUse = split[0] + '//canary.' + split[1] | ||||
|           } | ||||
| 
 | ||||
|           const requestData = this.createRequestBody(body, method) | ||||
| 
 | ||||
|           const response = await fetch(urlToUse, requestData) | ||||
|           const bucketIDFromHeaders = this.processHeaders(url, response.headers) | ||||
|           this.handleStatusCode(response, errorStack) | ||||
| 
 | ||||
|           // Sometimes Discord returns an empty 204 response that can't be made to JSON.
 | ||||
|           if (response.status === 204) return resolve(undefined) | ||||
| 
 | ||||
|           const json = await response.json() | ||||
|           if ( | ||||
|             json.retry_after !== undefined || | ||||
|             json.message === 'You are being rate limited.' | ||||
|           ) { | ||||
|             if (retryCount > 10) { | ||||
|               throw new Error('Max RateLimit Retries hit') | ||||
|             } | ||||
| 
 | ||||
|             return { | ||||
|               rateLimited: json.retry_after, | ||||
|               beforeFetch: false, | ||||
|               bucketID: bucketIDFromHeaders | ||||
|             } | ||||
|           } | ||||
|           return resolve(json) | ||||
|         } catch (error) { | ||||
|           return reject(error) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       this.addToQueue({ | ||||
|         callback, | ||||
|         bucketID, | ||||
|         url | ||||
|       this.rateLimits.set(url, { | ||||
|         url, | ||||
|         resetAt: Number(resetAt) * 1000, | ||||
|         bucket | ||||
|       }) | ||||
|       if (!this.queueInProcess) { | ||||
|         this.queueInProcess = true | ||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||
|         this.processQueue() | ||||
| 
 | ||||
|       if (bucket !== null) { | ||||
|         this.rateLimits.set(bucket, { | ||||
|           url, | ||||
|           resetAt: Number(resetAt) * 1000, | ||||
|           bucket | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async logErrors (response: Response, errorStack?: unknown): Promise<void> { | ||||
|     try { | ||||
|       const error = await response.json() | ||||
|       console.error(error) | ||||
|     } catch { | ||||
|       console.error(response) | ||||
|     } | ||||
| 
 | ||||
|     if (global !== null) { | ||||
|       const reset = Date.now() + Number(retryAfter) | ||||
|       this.globalRateLimit = true | ||||
|       rateLimited = true | ||||
| 
 | ||||
|       this.rateLimits.set('global', { | ||||
|         url: 'global', | ||||
|         resetAt: reset, | ||||
|         bucket | ||||
|       }) | ||||
| 
 | ||||
|       if (bucket !== null) { | ||||
|         this.rateLimits.set(bucket, { | ||||
|           url: 'global', | ||||
|           resetAt: reset, | ||||
|           bucket | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return rateLimited ? bucket : undefined | ||||
|   } | ||||
| 
 | ||||
|   handleStatusCode ( | ||||
|     response: Response, | ||||
|     errorStack?: unknown | ||||
|   handleStatusCode( | ||||
|     response: Response | ||||
|   ): undefined | boolean { | ||||
|     const status = response.status | ||||
| 
 | ||||
|  | @ -324,9 +264,6 @@ export class RESTManager { | |||
|       return true | ||||
|     } | ||||
| 
 | ||||
|     // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||
|     this.logErrors(response, errorStack) | ||||
| 
 | ||||
|     if (status === HttpResponseCode.Unauthorized) | ||||
|       throw new Error('Request was not successful. Invalid Token.') | ||||
| 
 | ||||
|  | @ -345,76 +282,112 @@ export class RESTManager { | |||
|     throw new Error('Request Unknown Error') | ||||
|   } | ||||
| 
 | ||||
|   processHeaders (url: string, headers: Headers): string | null | undefined { | ||||
|     let ratelimited = false | ||||
|   async make( | ||||
|     method: RequestMethods, | ||||
|     url: string, | ||||
|     body?: unknown, | ||||
|     retryCount = 0, | ||||
|     bucket?: string | null | ||||
|   ): Promise<any> { | ||||
|     return await new Promise((resolve, reject) => { | ||||
|       const onComplete = async (): Promise<undefined | any> => { | ||||
|         try { | ||||
|           const rateLimitResetIn = await this.isRateLimited(url) | ||||
|           if (rateLimitResetIn !== false) { | ||||
|             return { | ||||
|               rateLimited: rateLimitResetIn, | ||||
|               before: true, | ||||
|               bucket | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|     // Get all useful headers
 | ||||
|     const remaining = headers.get('x-ratelimit-remaining') | ||||
|     const resetTimestamp = headers.get('x-ratelimit-reset') | ||||
|     const retryAfter = headers.get('retry-after') | ||||
|     const global = headers.get('x-ratelimit-global') | ||||
|     const bucketID = headers.get('x-ratelimit-bucket') | ||||
|           const query = | ||||
|             method === 'get' && body !== undefined | ||||
|               ? Object.entries(body as any) | ||||
|                 .map( | ||||
|                   ([key, value]) => | ||||
|                     `${encodeURIComponent(key)}=${encodeURIComponent( | ||||
|                       value as any | ||||
|                     )}` | ||||
|                 ) | ||||
|                 .join('&') | ||||
|               : '' | ||||
|           let urlToUse = | ||||
|             method === 'get' && query !== '' ? `${url}?${query}` : url | ||||
| 
 | ||||
|     // If there is no remaining rate limit for this endpoint, we save it in cache
 | ||||
|     if (remaining !== null && remaining === '0') { | ||||
|       ratelimited = true | ||||
|           if (this.client.canary === true) { | ||||
|             const split = urlToUse.split('//') | ||||
|             urlToUse = split[0] + '//canary.' + split[1] | ||||
|           } | ||||
| 
 | ||||
|       this.ratelimitedPaths.set(url, { | ||||
|         url, | ||||
|         resetTimestamp: Number(resetTimestamp) * 1000, | ||||
|         bucketID | ||||
|       }) | ||||
|           const requestData = this.prepare(body, method) | ||||
| 
 | ||||
|       if (bucketID !== null) { | ||||
|         this.ratelimitedPaths.set(bucketID, { | ||||
|           url, | ||||
|           resetTimestamp: Number(resetTimestamp) * 1000, | ||||
|           bucketID | ||||
|         }) | ||||
|           const response = await fetch(urlToUse, requestData) | ||||
|           const bucketFromHeaders = this.processHeaders(url, response.headers) | ||||
|           this.handleStatusCode(response) | ||||
| 
 | ||||
|           if (response.status === 204) return resolve(undefined) | ||||
| 
 | ||||
|           const json = await response.json() | ||||
|           if ( | ||||
|             json.retry_after !== undefined || | ||||
|             json.message === 'You are being rate limited.' | ||||
|           ) { | ||||
|             if (retryCount > 10) { | ||||
|               throw new Error('Max RateLimit Retries hit') | ||||
|             } | ||||
| 
 | ||||
|             return { | ||||
|               rateLimited: json.retry_after, | ||||
|               before: false, | ||||
|               bucket: bucketFromHeaders | ||||
|             } | ||||
|           } | ||||
|           return resolve(json) | ||||
|         } catch (error) { | ||||
|           return reject(error) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // If there is no remaining global limit, we save it in cache
 | ||||
|     if (global !== null) { | ||||
|       const reset = Date.now() + Number(retryAfter) | ||||
|       this.globallyRateLimited = true | ||||
|       ratelimited = true | ||||
| 
 | ||||
|       this.ratelimitedPaths.set('global', { | ||||
|         url: 'global', | ||||
|         resetTimestamp: reset, | ||||
|         bucketID | ||||
|       this.queue({ | ||||
|         onComplete, | ||||
|         bucket, | ||||
|         url | ||||
|       }) | ||||
| 
 | ||||
|       if (bucketID !== null) { | ||||
|         this.ratelimitedPaths.set(bucketID, { | ||||
|           url: 'global', | ||||
|           resetTimestamp: reset, | ||||
|           bucketID | ||||
|         }) | ||||
|       if (!this.processing) { | ||||
|         this.processing = true | ||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||
|         this.processQueue() | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return ratelimited ? bucketID : undefined | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async get (url: string, body?: unknown): Promise<any> { | ||||
|     return await this.runMethod('get', url, body) | ||||
|   async handleRateLimits(): Promise<void> { | ||||
|     const now = Date.now() | ||||
|     this.rateLimits.forEach((value, key) => { | ||||
|       if (value.resetAt > now) return | ||||
|       this.rateLimits.delete(key) | ||||
|       if (key === 'global') this.globalRateLimit = false | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async post (url: string, body?: unknown): Promise<any> { | ||||
|     return await this.runMethod('post', url, body) | ||||
|   async get(url: string, body?: unknown): Promise<any> { | ||||
|     return await this.make('get', url, body) | ||||
|   } | ||||
| 
 | ||||
|   async delete (url: string, body?: unknown): Promise<any> { | ||||
|     return await this.runMethod('delete', url, body) | ||||
|   async post(url: string, body?: unknown): Promise<any> { | ||||
|     return await this.make('post', url, body) | ||||
|   } | ||||
| 
 | ||||
|   async patch (url: string, body?: unknown): Promise<any> { | ||||
|     return await this.runMethod('patch', url, body) | ||||
|   async delete(url: string, body?: unknown): Promise<any> { | ||||
|     return await this.make('delete', url, body) | ||||
|   } | ||||
| 
 | ||||
|   async put (url: string, body?: unknown): Promise<any> { | ||||
|     return await this.runMethod('put', url, body) | ||||
|   async patch(url: string, body?: unknown): Promise<any> { | ||||
|     return await this.make('patch', url, body) | ||||
|   } | ||||
| 
 | ||||
|   async put(url: string, body?: unknown): Promise<any> { | ||||
|     return await this.make('put', url, body) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import { Client } from '../models/client.ts' | |||
| import { EmojiPayload } from '../types/emoji.ts' | ||||
| import { USER } from '../types/endpoint.ts' | ||||
| import { Base } from './base.ts' | ||||
| import { Guild } from "./guild.ts" | ||||
| import { User } from './user.ts' | ||||
| 
 | ||||
| export class Emoji extends Base { | ||||
|  | @ -9,6 +10,7 @@ export class Emoji extends Base { | |||
|   name: string | ||||
|   roles?: string[] | ||||
|   user?: User | ||||
|   guild?: Guild | ||||
|   requireColons?: boolean | ||||
|   managed?: boolean | ||||
|   animated?: boolean | ||||
|  | @ -20,17 +22,16 @@ export class Emoji extends Base { | |||
|     } else return `<a:${this.name}:${this.id}>` | ||||
|   } | ||||
| 
 | ||||
|   toString(): string { | ||||
|     return this.getEmojiString | ||||
|   } | ||||
| 
 | ||||
|   constructor (client: Client, data: EmojiPayload) { | ||||
|     super(client, data) | ||||
|     this.id = data.id | ||||
|     this.name = data.name | ||||
|     if(data.user !== undefined) this.user = new User(this.client, data.user) | ||||
|     this.roles = data.roles | ||||
|     if (data.user !== undefined) { | ||||
|       User.autoInit(this.client, { | ||||
|         endpoint: USER, | ||||
|         restURLfuncArgs: [data.user.id] | ||||
|       }).then(user => (this.user = user)) | ||||
|     } | ||||
|     this.requireColons = data.require_colons | ||||
|     this.managed = data.managed | ||||
|     this.animated = data.animated | ||||
|  |  | |||
|  | @ -6,7 +6,8 @@ import { VoiceState } from './voiceState.ts' | |||
| import { RolesManager } from '../managers/roles.ts' | ||||
| import { GuildChannelsManager } from '../managers/guildChannels.ts' | ||||
| import { MembersManager } from '../managers/members.ts' | ||||
| import { Emoji } from "./emoji.ts" | ||||
| import { Role } from "./role.ts" | ||||
| import { GuildEmojisManager } from "../managers/guildEmojis.ts" | ||||
| 
 | ||||
| export class Guild extends Base { | ||||
|   id: string | ||||
|  | @ -27,7 +28,7 @@ export class Guild extends Base { | |||
|   defaultMessageNotifications?: string | ||||
|   explicitContentFilter?: string | ||||
|   roles: RolesManager | ||||
|   emojis: Emoji[] = [] | ||||
|   emojis: GuildEmojisManager | ||||
|   features?: GuildFeatures[] | ||||
|   mfaLevel?: string | ||||
|   applicationID?: string | ||||
|  | @ -66,6 +67,7 @@ export class Guild extends Base { | |||
|       this | ||||
|     ) | ||||
|     this.roles = new RolesManager(this.client, this) | ||||
|     this.emojis = new GuildEmojisManager(this.client, this.client.emojis, this) | ||||
| 
 | ||||
|     if (!this.unavailable) { | ||||
|       this.name = data.name | ||||
|  | @ -208,4 +210,8 @@ export class Guild extends Base { | |||
|         data.approximate_presence_count ?? this.approximatePresenceCount | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async getEveryoneRole(): Promise<Role> { | ||||
|     return (await this.roles.array().then(arr => arr?.sort((b, a) => a.position - b.position)[0]) as any) as Role | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import { CommandClient, Intents } from '../../mod.ts' | ||||
| import PingCommand from './cmds/ping.ts' | ||||
| import AddEmojiCommand from './cmds/addemoji.ts' | ||||
| import UserinfoCommand from './cmds/userinfo.ts' | ||||
| import { TOKEN } from './config.ts' | ||||
| 
 | ||||
|  | @ -14,9 +15,10 @@ client.on('ready', () => { | |||
|   console.log(`[Login] Logged in as ${client.user?.tag}!`) | ||||
| }) | ||||
| 
 | ||||
| client.on("commandError", console.log) | ||||
| client.on("commandError", console.error) | ||||
| 
 | ||||
| client.commands.add(PingCommand) | ||||
| client.commands.add(UserinfoCommand) | ||||
| client.commands.add(AddEmojiCommand) | ||||
| 
 | ||||
| client.connect(TOKEN, Intents.All) | ||||
							
								
								
									
										22
									
								
								src/test/cmds/addemoji.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/test/cmds/addemoji.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| import { Command } from '../../../mod.ts' | ||||
| import { CommandContext } from '../../models/command.ts' | ||||
| 
 | ||||
| export default class AddEmojiCommand extends Command { | ||||
|   name = 'addemoji' | ||||
|   aliases = [ 'ae', 'emojiadd' ] | ||||
|   args = 2 | ||||
|   guildOnly = true | ||||
| 
 | ||||
|   execute(ctx: CommandContext): any { | ||||
|     const name = ctx.args[0] | ||||
|     if(name === undefined) return ctx.message.reply('No name was given!') | ||||
|     const url = ctx.argString.slice(name.length).trim() | ||||
|     if(url === '') return ctx.message.reply('No URL was given!') | ||||
|     ctx.message.guild?.emojis.create(name, url).then(emoji => { | ||||
|         if(emoji === undefined) throw new Error('Unknown') | ||||
|         ctx.message.reply(`Successfuly added emoji ${emoji.toString()} ${emoji.name}!`) | ||||
|     }).catch(e => { | ||||
|         ctx.message.reply(`Failed to add emoji. Reason: ${e.message}`) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | @ -14,6 +14,8 @@ const GUILD_WIDGET = (guildID: string): string => | |||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/widget` | ||||
| const GUILD_EMOJI = (guildID: string, emojiID: string): string => | ||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/emojis/${emojiID}` | ||||
| const GUILD_EMOJIS = (guildID: string): string => | ||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/emojis` | ||||
| const GUILD_ROLE = (guildID: string, roleID: string): string => | ||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/roles/${roleID}` | ||||
| const GUILD_ROLES = (guildID: string): string => | ||||
|  | @ -172,8 +174,6 @@ const TEAM_ICON = (teamID: string, iconID: string): string => | |||
| // Emoji Endpoints
 | ||||
| const EMOJI = (guildID: string, emojiID: string): string => | ||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/emojis/${emojiID}` | ||||
| const EMOJIS = (guildID: string): string => | ||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/emojis` | ||||
| 
 | ||||
| // Template Endpoint
 | ||||
| const TEMPLATE = (templateCODE: string): string => | ||||
|  | @ -259,7 +259,7 @@ export default [ | |||
|   ACHIEVEMENT_ICON, | ||||
|   TEAM_ICON, | ||||
|   EMOJI, | ||||
|   EMOJIS, | ||||
|   GUILD_EMOJIS, | ||||
|   TEMPLATE, | ||||
|   INVITE, | ||||
|   VOICE_REGIONS | ||||
|  | @ -305,6 +305,7 @@ export { | |||
|   CHANNEL_PIN, | ||||
|   CHANNEL_PINS, | ||||
|   CHANNEL_PERMISSION, | ||||
|   GUILD_EMOJIS, | ||||
|   CHANNEL_TYPING, | ||||
|   GROUP_RECIPIENT, | ||||
|   CURRENT_USER, | ||||
|  | @ -333,7 +334,6 @@ export { | |||
|   ACHIEVEMENT_ICON, | ||||
|   TEAM_ICON, | ||||
|   EMOJI, | ||||
|   EMOJIS, | ||||
|   TEMPLATE, | ||||
|   INVITE, | ||||
|   VOICE_REGIONS | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue