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) |     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> { |   async fetch (guildID: string, id: string): Promise<Emoji> { | ||||||
|     return await new Promise((resolve, reject) => { |     return await new Promise((resolve, reject) => { | ||||||
|       this.client.rest |       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 * as baseEndpoints from '../consts/urlsAndVersions.ts' | ||||||
| import { Client } from './client.ts' | import { Client } from './client.ts' | ||||||
| import { getBuildInfo } from '../utils/buildInfo.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 { | export enum HttpResponseCode { | ||||||
|   Ok = 200, |   Ok = 200, | ||||||
|  | @ -17,122 +20,102 @@ export enum HttpResponseCode { | ||||||
|   GatewayUnavailable = 502 |   GatewayUnavailable = 502 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type RequestMethods = | export interface RequestHeaders { | ||||||
|   | 'get' |   [name: string]: string | ||||||
|   | 'post' | } | ||||||
|   | 'put' |  | ||||||
|   | 'patch' |  | ||||||
|   | 'head' |  | ||||||
|   | 'delete' |  | ||||||
| 
 | 
 | ||||||
| export interface QueuedRequest { | export interface QueuedItem { | ||||||
|   callback: () => Promise< |   onComplete: () => Promise<{ | ||||||
|     | { |  | ||||||
|     rateLimited: any |     rateLimited: any | ||||||
|         beforeFetch: boolean |     bucket?: string | null | ||||||
|         bucketID?: string | null |     before: boolean | ||||||
|       } |   } | undefined> | ||||||
|     | undefined |   bucket?: string | null | ||||||
|   > |  | ||||||
|   bucketID?: string | null |  | ||||||
|   url: string |   url: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface RateLimitedPath { | export interface RateLimit { | ||||||
|   url: string |   url: string | ||||||
|   resetTimestamp: number |   resetAt: number | ||||||
|   bucketID: string | null |   bucket: string | null | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class RESTManager { | export class RESTManager { | ||||||
|   client: Client |   client: Client | ||||||
|   globallyRateLimited: boolean = false |   queues: { [key: string]: QueuedItem[] } = {} | ||||||
|   queueInProcess: boolean = false |   rateLimits = new Collection<string, RateLimit>() | ||||||
|   pathQueues: { [key: string]: QueuedRequest[] } = {} |   globalRateLimit: boolean = false | ||||||
|   ratelimitedPaths = new Map<string, RateLimitedPath>() |   processing: boolean = false | ||||||
| 
 | 
 | ||||||
|   constructor (client: Client) { |   constructor(client: Client) { | ||||||
|     this.client = client |     this.client = client | ||||||
|     setTimeout(() => this.processRateLimitedPaths, 1000) |     // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||||
|  |     this.handleRateLimits() | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async processRateLimitedPaths (): Promise<void> { |   async checkQueues(): Promise<void> { | ||||||
|     const now = Date.now() |     Object.entries(this.queues).forEach(([key, value]) => { | ||||||
|     this.ratelimitedPaths.forEach((value, key) => { |       if (value.length === 0) { | ||||||
|       if (value.resetTimestamp > now) return |         // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
 | ||||||
|       this.ratelimitedPaths.delete(key) |         delete this.queues[key] | ||||||
|       if (key === 'global') this.globallyRateLimited = false |       } | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   addToQueue (request: QueuedRequest): void { |   queue(request: QueuedItem): void { | ||||||
|     const route = request.url.substring( |     const route = request.url.substring( | ||||||
|       // eslint seriously?
 |       // eslint seriously?
 | ||||||
|       // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
 |       // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
 | ||||||
|       baseEndpoints.DISCORD_API_URL.length + 1 |       baseEndpoints.DISCORD_API_URL.length + 1 | ||||||
|     ) |     ) | ||||||
|     const parts = route.split('/') |     const parts = route.split('/') | ||||||
|     // Remove the major param
 |  | ||||||
|     parts.shift() |     parts.shift() | ||||||
|     const [id] = parts |     const [id] = parts | ||||||
| 
 | 
 | ||||||
|     if (this.pathQueues[id] !== undefined) { |     if (this.queues[id] !== undefined) { | ||||||
|       this.pathQueues[id].push(request) |       this.queues[id].push(request) | ||||||
|     } else { |     } else { | ||||||
|       this.pathQueues[id] = [request] |       this.queues[id] = [request] | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async cleanupQueues (): Promise<void> { |   async processQueue(): Promise<void> { | ||||||
|     Object.entries(this.pathQueues).forEach(([key, value]) => { |     if (Object.keys(this.queues).length !== 0 && !this.globalRateLimit) { | ||||||
|       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 |  | ||||||
|     ) { |  | ||||||
|       await Promise.allSettled( |       await Promise.allSettled( | ||||||
|         Object.values(this.pathQueues).map(async pathQueue => { |         Object.values(this.queues).map(async pathQueue => { | ||||||
|           const request = pathQueue.shift() |           const request = pathQueue.shift() | ||||||
|           if (request === undefined) return |           if (request === undefined) return | ||||||
| 
 | 
 | ||||||
|           const rateLimitedURLResetIn = await this.checkRatelimits(request.url) |           const rateLimitedURLResetIn = await this.isRateLimited(request.url) | ||||||
| 
 | 
 | ||||||
|           if (typeof request.bucketID === 'string') { |           if (typeof request.bucket === 'string') { | ||||||
|             const rateLimitResetIn = await this.checkRatelimits( |             const rateLimitResetIn = await this.isRateLimited( | ||||||
|               request.bucketID |               request.bucket | ||||||
|             ) |             ) | ||||||
|             if (rateLimitResetIn !== false) { |             if (rateLimitResetIn !== false) { | ||||||
|               // This request is still rate limited read to queue
 |               // This request is still rate limited read to queue
 | ||||||
|               this.addToQueue(request) |               this.queue(request) | ||||||
|             } else { |             } else { | ||||||
|               // This request is not rate limited so it should be run
 |               // 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) { |               if (result?.rateLimited !== undefined) { | ||||||
|                 this.addToQueue({ |                 this.queue({ | ||||||
|                   ...request, |                   ...request, | ||||||
|                   bucketID: result.bucketID ?? request.bucketID |                   bucket: result.bucket ?? request.bucket | ||||||
|                 }) |                 }) | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           } else { |           } else { | ||||||
|             if (rateLimitedURLResetIn !== false) { |             if (rateLimitedURLResetIn !== false) { | ||||||
|               // This URL is rate limited readd to queue
 |               // This URL is rate limited readd to queue
 | ||||||
|               this.addToQueue(request) |               this.queue(request) | ||||||
|             } else { |             } else { | ||||||
|               // This request has no bucket id so it should be processed
 |               // 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) { |               if (result?.rateLimited !== undefined) { | ||||||
|                 this.addToQueue({ |                 this.queue({ | ||||||
|                   ...request, |                   ...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) |       await delay(1000) | ||||||
|       // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |       // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||||
|       this.processQueue() |       this.processQueue() | ||||||
|       // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |       // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||||
|       this.cleanupQueues() |       this.checkQueues() | ||||||
|     } else this.queueInProcess = false |     } else this.processing = false | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   createRequestBody ( |   prepare( | ||||||
|     body: any, |     body: any, | ||||||
|     method: RequestMethods |     method: RequestMethods | ||||||
|   ): { [key: string]: any } { |   ): { [key: string]: any } { | ||||||
|     const headers: { [key: string]: string } = { | 
 | ||||||
|       Authorization: `Bot ${this.client.token}`, |     const headers: RequestHeaders = { | ||||||
|       'User-Agent': `DiscordBot (harmony)` |       'Authorization': `Bot ${this.client.token}`, | ||||||
|  |       'User-Agent': `DiscordBot (harmony, https://github.com/harmony-org/harmony)` | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (this.client.token === undefined) delete headers.Authorization |     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) { |     if (body?.reason !== undefined) { | ||||||
|       headers['X-Audit-Log-Reason'] = encodeURIComponent(body.reason) |       headers['X-Audit-Log-Reason'] = encodeURIComponent(body.reason) | ||||||
|  | @ -203,40 +187,117 @@ export class RESTManager { | ||||||
|     return data |     return data | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async checkRatelimits (url: string): Promise<number | false> { |   async isRateLimited(url: string): Promise<number | false> { | ||||||
|     const ratelimited = this.ratelimitedPaths.get(url) |     const global = this.rateLimits.get('global') | ||||||
|     const global = this.ratelimitedPaths.get('global') |     const rateLimited = this.rateLimits.get(url) | ||||||
|     const now = Date.now() |     const now = Date.now() | ||||||
| 
 | 
 | ||||||
|     if (ratelimited !== undefined && now < ratelimited.resetTimestamp) { |     if (rateLimited !== undefined && now < rateLimited.resetAt) { | ||||||
|       return ratelimited.resetTimestamp - now |       return rateLimited.resetAt - now | ||||||
|     } |     } | ||||||
|     if (global !== undefined && now < global.resetTimestamp) { |     if (global !== undefined && now < global.resetAt) { | ||||||
|       return global.resetTimestamp - now |       return global.resetAt - now | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return false |     return false | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async runMethod ( |   processHeaders(url: string, headers: Headers): string | null | undefined { | ||||||
|  |     let rateLimited = false | ||||||
|  | 
 | ||||||
|  |     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') | ||||||
|  | 
 | ||||||
|  |     if (remaining !== null && remaining === '0') { | ||||||
|  |       rateLimited = true | ||||||
|  | 
 | ||||||
|  |       this.rateLimits.set(url, { | ||||||
|  |         url, | ||||||
|  |         resetAt: Number(resetAt) * 1000, | ||||||
|  |         bucket | ||||||
|  |       }) | ||||||
|  | 
 | ||||||
|  |       if (bucket !== null) { | ||||||
|  |         this.rateLimits.set(bucket, { | ||||||
|  |           url, | ||||||
|  |           resetAt: Number(resetAt) * 1000, | ||||||
|  |           bucket | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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 | ||||||
|  |   ): undefined | boolean { | ||||||
|  |     const status = response.status | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       (status >= 200 && status < 400) || | ||||||
|  |       status === HttpResponseCode.TooManyRequests | ||||||
|  |     ) { | ||||||
|  |       return true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (status === HttpResponseCode.Unauthorized) | ||||||
|  |       throw new Error('Request was not successful. Invalid Token.') | ||||||
|  | 
 | ||||||
|  |     switch (status) { | ||||||
|  |       case HttpResponseCode.BadRequest: | ||||||
|  |       case HttpResponseCode.Unauthorized: | ||||||
|  |       case HttpResponseCode.Forbidden: | ||||||
|  |       case HttpResponseCode.NotFound: | ||||||
|  |       case HttpResponseCode.MethodNotAllowed: | ||||||
|  |         throw new Error('Request Client Error.') | ||||||
|  |       case HttpResponseCode.GatewayUnavailable: | ||||||
|  |         throw new Error('Request Server Error.') | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // left are all unknown
 | ||||||
|  |     throw new Error('Request Unknown Error') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async make( | ||||||
|     method: RequestMethods, |     method: RequestMethods, | ||||||
|     url: string, |     url: string, | ||||||
|     body?: unknown, |     body?: unknown, | ||||||
|     retryCount = 0, |     retryCount = 0, | ||||||
|     bucketID?: string | null |     bucket?: string | null | ||||||
|   ): Promise<any> { |   ): Promise<any> { | ||||||
|     const errorStack = new Error('Location In Your Files:') |  | ||||||
|     Error.captureStackTrace(errorStack) |  | ||||||
| 
 |  | ||||||
|     return await new Promise((resolve, reject) => { |     return await new Promise((resolve, reject) => { | ||||||
|       const callback = async (): Promise<undefined | any> => { |       const onComplete = async (): Promise<undefined | any> => { | ||||||
|         try { |         try { | ||||||
|           const rateLimitResetIn = await this.checkRatelimits(url) |           const rateLimitResetIn = await this.isRateLimited(url) | ||||||
|           if (rateLimitResetIn !== false) { |           if (rateLimitResetIn !== false) { | ||||||
|             return { |             return { | ||||||
|               rateLimited: rateLimitResetIn, |               rateLimited: rateLimitResetIn, | ||||||
|               beforeFetch: true, |               before: true, | ||||||
|               bucketID |               bucket | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|  | @ -259,13 +320,12 @@ export class RESTManager { | ||||||
|             urlToUse = split[0] + '//canary.' + split[1] |             urlToUse = split[0] + '//canary.' + split[1] | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           const requestData = this.createRequestBody(body, method) |           const requestData = this.prepare(body, method) | ||||||
| 
 | 
 | ||||||
|           const response = await fetch(urlToUse, requestData) |           const response = await fetch(urlToUse, requestData) | ||||||
|           const bucketIDFromHeaders = this.processHeaders(url, response.headers) |           const bucketFromHeaders = this.processHeaders(url, response.headers) | ||||||
|           this.handleStatusCode(response, errorStack) |           this.handleStatusCode(response) | ||||||
| 
 | 
 | ||||||
|           // Sometimes Discord returns an empty 204 response that can't be made to JSON.
 |  | ||||||
|           if (response.status === 204) return resolve(undefined) |           if (response.status === 204) return resolve(undefined) | ||||||
| 
 | 
 | ||||||
|           const json = await response.json() |           const json = await response.json() | ||||||
|  | @ -279,8 +339,8 @@ export class RESTManager { | ||||||
| 
 | 
 | ||||||
|             return { |             return { | ||||||
|               rateLimited: json.retry_after, |               rateLimited: json.retry_after, | ||||||
|               beforeFetch: false, |               before: false, | ||||||
|               bucketID: bucketIDFromHeaders |               bucket: bucketFromHeaders | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|           return resolve(json) |           return resolve(json) | ||||||
|  | @ -289,132 +349,45 @@ export class RESTManager { | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       this.addToQueue({ |       this.queue({ | ||||||
|         callback, |         onComplete, | ||||||
|         bucketID, |         bucket, | ||||||
|         url |         url | ||||||
|       }) |       }) | ||||||
|       if (!this.queueInProcess) { |       if (!this.processing) { | ||||||
|         this.queueInProcess = true |         this.processing = true | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 |         // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | ||||||
|         this.processQueue() |         this.processQueue() | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async logErrors (response: Response, errorStack?: unknown): Promise<void> { |   async handleRateLimits(): Promise<void> { | ||||||
|     try { |     const now = Date.now() | ||||||
|       const error = await response.json() |     this.rateLimits.forEach((value, key) => { | ||||||
|       console.error(error) |       if (value.resetAt > now) return | ||||||
|     } catch { |       this.rateLimits.delete(key) | ||||||
|       console.error(response) |       if (key === 'global') this.globalRateLimit = false | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   handleStatusCode ( |  | ||||||
|     response: Response, |  | ||||||
|     errorStack?: unknown |  | ||||||
|   ): undefined | boolean { |  | ||||||
|     const status = response.status |  | ||||||
| 
 |  | ||||||
|     if ( |  | ||||||
|       (status >= 200 && status < 400) || |  | ||||||
|       status === HttpResponseCode.TooManyRequests |  | ||||||
|     ) { |  | ||||||
|       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.') |  | ||||||
| 
 |  | ||||||
|     switch (status) { |  | ||||||
|       case HttpResponseCode.BadRequest: |  | ||||||
|       case HttpResponseCode.Unauthorized: |  | ||||||
|       case HttpResponseCode.Forbidden: |  | ||||||
|       case HttpResponseCode.NotFound: |  | ||||||
|       case HttpResponseCode.MethodNotAllowed: |  | ||||||
|         throw new Error('Request Client Error.') |  | ||||||
|       case HttpResponseCode.GatewayUnavailable: |  | ||||||
|         throw new Error('Request Server Error.') |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // left are all unknown
 |  | ||||||
|     throw new Error('Request Unknown Error') |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   processHeaders (url: string, headers: Headers): string | null | undefined { |  | ||||||
|     let ratelimited = false |  | ||||||
| 
 |  | ||||||
|     // 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') |  | ||||||
| 
 |  | ||||||
|     // If there is no remaining rate limit for this endpoint, we save it in cache
 |  | ||||||
|     if (remaining !== null && remaining === '0') { |  | ||||||
|       ratelimited = true |  | ||||||
| 
 |  | ||||||
|       this.ratelimitedPaths.set(url, { |  | ||||||
|         url, |  | ||||||
|         resetTimestamp: Number(resetTimestamp) * 1000, |  | ||||||
|         bucketID |  | ||||||
|       }) |  | ||||||
| 
 |  | ||||||
|       if (bucketID !== null) { |  | ||||||
|         this.ratelimitedPaths.set(bucketID, { |  | ||||||
|           url, |  | ||||||
|           resetTimestamp: Number(resetTimestamp) * 1000, |  | ||||||
|           bucketID |  | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   async get(url: string, body?: unknown): Promise<any> { | ||||||
|  |     return await this.make('get', url, body) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|     // If there is no remaining global limit, we save it in cache
 |   async post(url: string, body?: unknown): Promise<any> { | ||||||
|     if (global !== null) { |     return await this.make('post', url, body) | ||||||
|       const reset = Date.now() + Number(retryAfter) |  | ||||||
|       this.globallyRateLimited = true |  | ||||||
|       ratelimited = true |  | ||||||
| 
 |  | ||||||
|       this.ratelimitedPaths.set('global', { |  | ||||||
|         url: 'global', |  | ||||||
|         resetTimestamp: reset, |  | ||||||
|         bucketID |  | ||||||
|       }) |  | ||||||
| 
 |  | ||||||
|       if (bucketID !== null) { |  | ||||||
|         this.ratelimitedPaths.set(bucketID, { |  | ||||||
|           url: 'global', |  | ||||||
|           resetTimestamp: reset, |  | ||||||
|           bucketID |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|     return ratelimited ? bucketID : undefined |   async delete(url: string, body?: unknown): Promise<any> { | ||||||
|  |     return await this.make('delete', url, body) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async get (url: string, body?: unknown): Promise<any> { |   async patch(url: string, body?: unknown): Promise<any> { | ||||||
|     return await this.runMethod('get', url, body) |     return await this.make('patch', url, body) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async post (url: string, body?: unknown): Promise<any> { |   async put(url: string, body?: unknown): Promise<any> { | ||||||
|     return await this.runMethod('post', url, body) |     return await this.make('put', url, body) | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async delete (url: string, body?: unknown): Promise<any> { |  | ||||||
|     return await this.runMethod('delete', url, body) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async patch (url: string, body?: unknown): Promise<any> { |  | ||||||
|     return await this.runMethod('patch', url, body) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async put (url: string, body?: unknown): Promise<any> { |  | ||||||
|     return await this.runMethod('put', url, body) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ import { Client } from '../models/client.ts' | ||||||
| import { EmojiPayload } from '../types/emoji.ts' | import { EmojiPayload } from '../types/emoji.ts' | ||||||
| import { USER } from '../types/endpoint.ts' | import { USER } from '../types/endpoint.ts' | ||||||
| import { Base } from './base.ts' | import { Base } from './base.ts' | ||||||
|  | import { Guild } from "./guild.ts" | ||||||
| import { User } from './user.ts' | import { User } from './user.ts' | ||||||
| 
 | 
 | ||||||
| export class Emoji extends Base { | export class Emoji extends Base { | ||||||
|  | @ -9,6 +10,7 @@ export class Emoji extends Base { | ||||||
|   name: string |   name: string | ||||||
|   roles?: string[] |   roles?: string[] | ||||||
|   user?: User |   user?: User | ||||||
|  |   guild?: Guild | ||||||
|   requireColons?: boolean |   requireColons?: boolean | ||||||
|   managed?: boolean |   managed?: boolean | ||||||
|   animated?: boolean |   animated?: boolean | ||||||
|  | @ -20,17 +22,16 @@ export class Emoji extends Base { | ||||||
|     } else return `<a:${this.name}:${this.id}>` |     } else return `<a:${this.name}:${this.id}>` | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   toString(): string { | ||||||
|  |     return this.getEmojiString | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   constructor (client: Client, data: EmojiPayload) { |   constructor (client: Client, data: EmojiPayload) { | ||||||
|     super(client, data) |     super(client, data) | ||||||
|     this.id = data.id |     this.id = data.id | ||||||
|     this.name = data.name |     this.name = data.name | ||||||
|  |     if(data.user !== undefined) this.user = new User(this.client, data.user) | ||||||
|     this.roles = data.roles |     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.requireColons = data.require_colons | ||||||
|     this.managed = data.managed |     this.managed = data.managed | ||||||
|     this.animated = data.animated |     this.animated = data.animated | ||||||
|  |  | ||||||
|  | @ -6,7 +6,8 @@ import { VoiceState } from './voiceState.ts' | ||||||
| import { RolesManager } from '../managers/roles.ts' | import { RolesManager } from '../managers/roles.ts' | ||||||
| import { GuildChannelsManager } from '../managers/guildChannels.ts' | import { GuildChannelsManager } from '../managers/guildChannels.ts' | ||||||
| import { MembersManager } from '../managers/members.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 { | export class Guild extends Base { | ||||||
|   id: string |   id: string | ||||||
|  | @ -27,7 +28,7 @@ export class Guild extends Base { | ||||||
|   defaultMessageNotifications?: string |   defaultMessageNotifications?: string | ||||||
|   explicitContentFilter?: string |   explicitContentFilter?: string | ||||||
|   roles: RolesManager |   roles: RolesManager | ||||||
|   emojis: Emoji[] = [] |   emojis: GuildEmojisManager | ||||||
|   features?: GuildFeatures[] |   features?: GuildFeatures[] | ||||||
|   mfaLevel?: string |   mfaLevel?: string | ||||||
|   applicationID?: string |   applicationID?: string | ||||||
|  | @ -66,6 +67,7 @@ export class Guild extends Base { | ||||||
|       this |       this | ||||||
|     ) |     ) | ||||||
|     this.roles = new RolesManager(this.client, this) |     this.roles = new RolesManager(this.client, this) | ||||||
|  |     this.emojis = new GuildEmojisManager(this.client, this.client.emojis, this) | ||||||
| 
 | 
 | ||||||
|     if (!this.unavailable) { |     if (!this.unavailable) { | ||||||
|       this.name = data.name |       this.name = data.name | ||||||
|  | @ -208,4 +210,8 @@ export class Guild extends Base { | ||||||
|         data.approximate_presence_count ?? this.approximatePresenceCount |         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 { CommandClient, Intents } from '../../mod.ts' | ||||||
| import PingCommand from './cmds/ping.ts' | import PingCommand from './cmds/ping.ts' | ||||||
|  | import AddEmojiCommand from './cmds/addemoji.ts' | ||||||
| import UserinfoCommand from './cmds/userinfo.ts' | import UserinfoCommand from './cmds/userinfo.ts' | ||||||
| import { TOKEN } from './config.ts' | import { TOKEN } from './config.ts' | ||||||
| 
 | 
 | ||||||
|  | @ -14,9 +15,10 @@ client.on('ready', () => { | ||||||
|   console.log(`[Login] Logged in as ${client.user?.tag}!`) |   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(PingCommand) | ||||||
| client.commands.add(UserinfoCommand) | client.commands.add(UserinfoCommand) | ||||||
|  | client.commands.add(AddEmojiCommand) | ||||||
| 
 | 
 | ||||||
| client.connect(TOKEN, Intents.All) | 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` |   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/widget` | ||||||
| const GUILD_EMOJI = (guildID: string, emojiID: string): string => | const GUILD_EMOJI = (guildID: string, emojiID: string): string => | ||||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/emojis/${emojiID}` |   `${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 => | const GUILD_ROLE = (guildID: string, roleID: string): string => | ||||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/roles/${roleID}` |   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/roles/${roleID}` | ||||||
| const GUILD_ROLES = (guildID: string): string => | const GUILD_ROLES = (guildID: string): string => | ||||||
|  | @ -172,8 +174,6 @@ const TEAM_ICON = (teamID: string, iconID: string): string => | ||||||
| // Emoji Endpoints
 | // Emoji Endpoints
 | ||||||
| const EMOJI = (guildID: string, emojiID: string): string => | const EMOJI = (guildID: string, emojiID: string): string => | ||||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/emojis/${emojiID}` |   `${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
 | // Template Endpoint
 | ||||||
| const TEMPLATE = (templateCODE: string): string => | const TEMPLATE = (templateCODE: string): string => | ||||||
|  | @ -259,7 +259,7 @@ export default [ | ||||||
|   ACHIEVEMENT_ICON, |   ACHIEVEMENT_ICON, | ||||||
|   TEAM_ICON, |   TEAM_ICON, | ||||||
|   EMOJI, |   EMOJI, | ||||||
|   EMOJIS, |   GUILD_EMOJIS, | ||||||
|   TEMPLATE, |   TEMPLATE, | ||||||
|   INVITE, |   INVITE, | ||||||
|   VOICE_REGIONS |   VOICE_REGIONS | ||||||
|  | @ -305,6 +305,7 @@ export { | ||||||
|   CHANNEL_PIN, |   CHANNEL_PIN, | ||||||
|   CHANNEL_PINS, |   CHANNEL_PINS, | ||||||
|   CHANNEL_PERMISSION, |   CHANNEL_PERMISSION, | ||||||
|  |   GUILD_EMOJIS, | ||||||
|   CHANNEL_TYPING, |   CHANNEL_TYPING, | ||||||
|   GROUP_RECIPIENT, |   GROUP_RECIPIENT, | ||||||
|   CURRENT_USER, |   CURRENT_USER, | ||||||
|  | @ -333,7 +334,6 @@ export { | ||||||
|   ACHIEVEMENT_ICON, |   ACHIEVEMENT_ICON, | ||||||
|   TEAM_ICON, |   TEAM_ICON, | ||||||
|   EMOJI, |   EMOJI, | ||||||
|   EMOJIS, |  | ||||||
|   TEMPLATE, |   TEMPLATE, | ||||||
|   INVITE, |   INVITE, | ||||||
|   VOICE_REGIONS |   VOICE_REGIONS | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue