RESTManager, CacheAdapters, and improvements
This commit is contained in:
parent
f319e0df91
commit
935456906d
|
@ -8,6 +8,7 @@ export const channelCreate: GatewayEventHandler = (
|
|||
const channel = getChannelByType(gateway.client, d)
|
||||
|
||||
if (channel !== undefined) {
|
||||
gateway.client.channels.set(d.id, d)
|
||||
gateway.client.emit('channelCreate', channel)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
import cache from '../../models/cache.ts'
|
||||
import { Channel } from '../../structures/channel.ts'
|
||||
|
||||
export const channelDelete: GatewayEventHandler = (
|
||||
gateway: Gateway,
|
||||
d: any
|
||||
) => {
|
||||
const channel: Channel = cache.get('channel', d.id)
|
||||
const channel: Channel = gateway.client.channels.get(d.id)
|
||||
if (channel !== undefined) {
|
||||
cache.del('channel', d.id)
|
||||
gateway.client.channels.delete(d.id)
|
||||
gateway.client.emit('channelDelete', channel)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
import cache from '../../models/cache.ts'
|
||||
import { TextChannel } from '../../structures/textChannel.ts'
|
||||
import { ChannelPayload } from "../../types/channelTypes.ts"
|
||||
|
||||
export const channelPinsUpdate: GatewayEventHandler = (
|
||||
gateway: Gateway,
|
||||
d: any
|
||||
) => {
|
||||
const after: TextChannel = cache.get('textchannel', d.channel_id)
|
||||
const after: TextChannel = gateway.client.channels.get(d.channel_id)
|
||||
if (after !== undefined) {
|
||||
const before = after.refreshFromData({
|
||||
last_pin_timestamp: d.last_pin_timestamp
|
||||
})
|
||||
let raw = gateway.client.channels._get(d.channel_id) as ChannelPayload;
|
||||
gateway.client.channels.set(after.id, Object.assign(raw, { last_pin_timestamp: d.last_pin_timestamp }))
|
||||
gateway.client.emit('channelPinsUpdate', before, after)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import cache from '../../models/cache.ts'
|
||||
import { Channel } from '../../structures/channel.ts'
|
||||
import getChannelByType from '../../utils/getChannelByType.ts'
|
||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
|
@ -7,9 +6,10 @@ export const channelUpdate: GatewayEventHandler = (
|
|||
gateway: Gateway,
|
||||
d: any
|
||||
) => {
|
||||
const oldChannel: Channel = cache.get('channel', d.id)
|
||||
const oldChannel: Channel = gateway.client.channels.get(d.id)
|
||||
|
||||
if (oldChannel !== undefined) {
|
||||
gateway.client.channels.set(d.id, d)
|
||||
if (oldChannel.type !== d.type) {
|
||||
const channel: Channel = getChannelByType(gateway.client, d) ?? oldChannel
|
||||
gateway.client.emit('channelUpdate', oldChannel, channel)
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
import cache from '../../models/cache.ts'
|
||||
import { Guild } from '../../structures/guild.ts'
|
||||
|
||||
export const guildCreate: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
||||
let guild: Guild = cache.get('guild', d.id)
|
||||
let guild: Guild | void = gateway.client.guilds.get(d.id)
|
||||
if (guild !== undefined) {
|
||||
// It was just lazy load, so we don't fire the event as its gonna fire for every guild bot is in
|
||||
gateway.client.guilds.set(d.id, d)
|
||||
guild.refreshFromData(d)
|
||||
} else {
|
||||
guild = new Guild(gateway.client, d)
|
||||
gateway.client.guilds.set(d.id, d)
|
||||
guild = gateway.client.guilds.get(d.id)
|
||||
gateway.client.emit('guildCreate', guild)
|
||||
}
|
||||
|
||||
gateway.client.emit('guildCreate', guild)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import cache from '../../models/cache.ts'
|
||||
import { Guild } from '../../structures/guild.ts'
|
||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
|
||||
export const guildDelte: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
||||
const guild: Guild = cache.get('guild', d.id)
|
||||
const guild: Guild | void = gateway.client.guilds.get(d.id)
|
||||
|
||||
if (guild !== undefined) {
|
||||
guild.refreshFromData(d)
|
||||
cache.del('guild', d.id)
|
||||
gateway.client.guilds.delete(d.id)
|
||||
gateway.client.emit('guildDelete', guild)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ import cache from '../../models/cache.ts'
|
|||
import { Guild } from '../../structures/guild.ts'
|
||||
|
||||
export const guildUpdate: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
||||
const after: Guild = cache.get('guild', d.id)
|
||||
if (after !== undefined) {
|
||||
const before: Guild = after.refreshFromData(d)
|
||||
gateway.client.emit('guildUpdate', before, after)
|
||||
}
|
||||
const before: Guild | void = gateway.client.guilds.get(d.id)
|
||||
if(!before) return
|
||||
gateway.client.guilds.set(d.id, d)
|
||||
const after: Guild | void = gateway.client.guilds.get(d.id)
|
||||
gateway.client.emit('guildUpdate', before, after)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Guild } from '../../structures/guild.ts'
|
||||
import { User } from '../../structures/user.ts'
|
||||
import { GuildPayload } from '../../types/guildTypes.ts'
|
||||
import { Gateway, GatewayEventHandler } from '../index.ts'
|
||||
|
@ -6,6 +5,9 @@ import { Gateway, GatewayEventHandler } from '../index.ts'
|
|||
export const ready: GatewayEventHandler = (gateway: Gateway, d: any) => {
|
||||
gateway.client.user = new User(gateway.client, d.user)
|
||||
gateway.sessionID = d.session_id
|
||||
d.guilds.forEach((guild: GuildPayload) => new Guild(gateway.client, guild))
|
||||
gateway.debug(`Received READY. Session: ${gateway.sessionID}`)
|
||||
d.guilds.forEach((guild: GuildPayload) => {
|
||||
gateway.client.guilds.set(guild.id, guild)
|
||||
})
|
||||
gateway.client.emit('ready')
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ import {
|
|||
import { GatewayResponse } from '../types/gatewayResponse.ts'
|
||||
import { GatewayOpcodes, GatewayIntents } from '../types/gatewayTypes.ts'
|
||||
import { gatewayHandlers } from './handlers/index.ts'
|
||||
import { GATEWAY_BOT } from '../types/endpoint.ts'
|
||||
import { GatewayBotPayload } from "../types/gatewayBot.ts"
|
||||
|
||||
/**
|
||||
* Handles Discord gateway connection.
|
||||
|
@ -24,7 +26,7 @@ class Gateway {
|
|||
heartbeatIntervalID?: number
|
||||
sequenceID?: number
|
||||
sessionID?: string
|
||||
lastPingTimestemp = 0
|
||||
lastPingTimestamp = 0
|
||||
private heartbeatServerResponded = false
|
||||
client: Client
|
||||
|
||||
|
@ -46,6 +48,7 @@ class Gateway {
|
|||
|
||||
private onopen (): void {
|
||||
this.connected = true
|
||||
this.debug("Connected to Gateway!")
|
||||
}
|
||||
|
||||
private onmessage (event: MessageEvent): void {
|
||||
|
@ -63,6 +66,7 @@ class Gateway {
|
|||
switch (op) {
|
||||
case GatewayOpcodes.HELLO:
|
||||
this.heartbeatInterval = d.heartbeat_interval
|
||||
this.debug(`Received HELLO. Heartbeat Interval: ${this.heartbeatInterval}`)
|
||||
this.heartbeatIntervalID = setInterval(() => {
|
||||
if (this.heartbeatServerResponded) {
|
||||
this.heartbeatServerResponded = false
|
||||
|
@ -79,7 +83,7 @@ class Gateway {
|
|||
d: this.sequenceID ?? null
|
||||
})
|
||||
)
|
||||
this.lastPingTimestemp = Date.now()
|
||||
this.lastPingTimestamp = Date.now()
|
||||
}, this.heartbeatInterval)
|
||||
|
||||
if (!this.initialized) {
|
||||
|
@ -92,7 +96,8 @@ class Gateway {
|
|||
|
||||
case GatewayOpcodes.HEARTBEAT_ACK:
|
||||
this.heartbeatServerResponded = true
|
||||
this.client.ping = Date.now() - this.lastPingTimestemp
|
||||
this.client.ping = Date.now() - this.lastPingTimestamp
|
||||
this.debug(`Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms`)
|
||||
break
|
||||
|
||||
case GatewayOpcodes.INVALID_SESSION:
|
||||
|
@ -135,7 +140,14 @@ class Gateway {
|
|||
console.log(eventError)
|
||||
}
|
||||
|
||||
private sendIdentify (): void {
|
||||
private async sendIdentify () {
|
||||
this.debug("Fetching /gateway/bot...")
|
||||
let info = await this.client.rest.get(GATEWAY_BOT()) as GatewayBotPayload
|
||||
if(info.session_start_limit.remaining == 0) throw new Error("Session Limit Reached. Retry After " + info.session_start_limit.reset_after + "ms")
|
||||
this.debug("Recommended Shards: " + info.shards)
|
||||
this.debug("=== Session Limit Info ===")
|
||||
this.debug(`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`)
|
||||
this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`)
|
||||
this.websocket.send(
|
||||
JSON.stringify({
|
||||
op: GatewayOpcodes.IDENTIFY,
|
||||
|
@ -164,6 +176,7 @@ class Gateway {
|
|||
}
|
||||
|
||||
private sendResume (): void {
|
||||
this.debug(`Preparing to resume with Session: ${this.sessionID}`)
|
||||
this.websocket.send(
|
||||
JSON.stringify({
|
||||
op: GatewayOpcodes.RESUME,
|
||||
|
@ -176,6 +189,10 @@ class Gateway {
|
|||
)
|
||||
}
|
||||
|
||||
debug(msg: string) {
|
||||
this.client.debug("Gateway", msg)
|
||||
}
|
||||
|
||||
initWebsocket (): void {
|
||||
this.websocket = new WebSocket(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { Client } from "../models/client.ts";
|
||||
import { Base } from "../structures/base.ts";
|
||||
|
||||
export class BaseManager<T, T2> {
|
||||
client: Client
|
||||
cacheName: string
|
||||
dataType: typeof Base
|
||||
|
||||
constructor(client: Client, cacheName: string, dataType: typeof Base) {
|
||||
this.client = client
|
||||
this.cacheName = cacheName
|
||||
this.dataType = dataType
|
||||
}
|
||||
|
||||
_get(key: string): T {
|
||||
return this.client.cache.get(this.cacheName, key) as T
|
||||
}
|
||||
|
||||
get(key: string): T2 | void {
|
||||
let raw = this._get(key)
|
||||
if(!raw) return
|
||||
return new this.dataType(this.client, raw) as any
|
||||
}
|
||||
|
||||
set(key: string, value: T) {
|
||||
return this.client.cache.set(this.cacheName, key, value)
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
return this.client.cache.delete(this.cacheName, key)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { Client } from "../models/client.ts";
|
||||
import { Channel } from "../structures/channel.ts";
|
||||
import { User } from "../structures/user.ts";
|
||||
import { ChannelPayload } from "../types/channelTypes.ts";
|
||||
import { CHANNEL } from "../types/endpoint.ts";
|
||||
import { BaseManager } from "./BaseManager.ts";
|
||||
|
||||
export class ChannelsManager extends BaseManager<ChannelPayload, Channel> {
|
||||
constructor(client: Client) {
|
||||
super(client, "channels", User)
|
||||
}
|
||||
|
||||
// Override get method as Generic
|
||||
get<T = Channel>(key: string): T {
|
||||
return new this.dataType(this.client, this._get(key)) as any
|
||||
}
|
||||
|
||||
fetch(id: string) {
|
||||
return new Promise((res, rej) => {
|
||||
this.client.rest.get(CHANNEL(id)).then(data => {
|
||||
this.set(id, data as ChannelPayload)
|
||||
res(new Channel(this.client, data as ChannelPayload))
|
||||
}).catch(e => rej(e))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Client } from "../models/client.ts";
|
||||
import { Emoji } from "../structures/emoji.ts";
|
||||
import { EmojiPayload } from "../types/emojiTypes.ts";
|
||||
import { CHANNEL } from "../types/endpoint.ts";
|
||||
import { BaseManager } from "./BaseManager.ts";
|
||||
|
||||
export class EmojisManager extends BaseManager<EmojiPayload, Emoji> {
|
||||
constructor(client: Client) {
|
||||
super(client, "emojis", Emoji)
|
||||
}
|
||||
|
||||
fetch(id: string) {
|
||||
return new Promise((res, rej) => {
|
||||
this.client.rest.get(CHANNEL(id)).then(data => {
|
||||
this.set(id, data as EmojiPayload)
|
||||
res(new Emoji(this.client, data as EmojiPayload))
|
||||
}).catch(e => rej(e))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Client } from "../models/client.ts";
|
||||
import { Guild } from "../structures/guild.ts";
|
||||
import { GUILD } from "../types/endpoint.ts";
|
||||
import { GuildPayload } from "../types/guildTypes.ts";
|
||||
import { BaseManager } from "./BaseManager.ts";
|
||||
|
||||
export class GuildManager extends BaseManager<GuildPayload, Guild> {
|
||||
constructor(client: Client) {
|
||||
super(client, "guilds", Guild)
|
||||
}
|
||||
|
||||
fetch(id: string) {
|
||||
return new Promise((res, rej) => {
|
||||
this.client.rest.get(GUILD(id)).then(data => {
|
||||
this.set(id, data as GuildPayload)
|
||||
res(new Guild(this.client, data as GuildPayload))
|
||||
}).catch(e => rej(e))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { Client } from "../models/client.ts";
|
||||
import { Guild } from "../structures/guild.ts";
|
||||
import { Role } from "../structures/role.ts";
|
||||
import { User } from "../structures/user.ts";
|
||||
import { GUILD_ROLE } from "../types/endpoint.ts";
|
||||
import { RolePayload } from "../types/roleTypes.ts";
|
||||
import { BaseManager } from "./BaseManager.ts";
|
||||
|
||||
export class RolesManager extends BaseManager<RolePayload, Role> {
|
||||
guild: Guild
|
||||
|
||||
constructor(client: Client, guild: Guild) {
|
||||
super(client, "roles:" + guild.id, Role)
|
||||
this.guild = guild
|
||||
}
|
||||
|
||||
fetch(id: string) {
|
||||
return new Promise((res, rej) => {
|
||||
this.client.rest.get(GUILD_ROLE(this.guild.id, id)).then(data => {
|
||||
this.set(id, data as RolePayload)
|
||||
res(new Role(this.client, data as RolePayload))
|
||||
}).catch(e => rej(e))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Client } from "../models/client.ts";
|
||||
import { User } from "../structures/user.ts";
|
||||
import { USER } from "../types/endpoint.ts";
|
||||
import { UserPayload } from "../types/userTypes.ts";
|
||||
import { BaseManager } from "./BaseManager.ts";
|
||||
|
||||
export class UserManager extends BaseManager<UserPayload, User> {
|
||||
constructor(client: Client) {
|
||||
super(client, "users", User)
|
||||
}
|
||||
|
||||
fetch(id: string) {
|
||||
return new Promise((res, rej) => {
|
||||
this.client.rest.get(USER(id)).then(data => {
|
||||
this.set(id, data as UserPayload)
|
||||
res(new User(this.client, data as UserPayload))
|
||||
}).catch(e => rej(e))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { Collection } from "../utils/collection.ts";
|
||||
import { Client } from "./client.ts";
|
||||
|
||||
export interface ICacheAdapter {
|
||||
client: Client
|
||||
get: (cacheName: string, key: string) => any
|
||||
set: (cacheName: string, key: string, value: any) => any
|
||||
delete: (cacheName: string, key: string) => boolean
|
||||
array: (cacheName: string) => void | any[]
|
||||
}
|
||||
|
||||
export class DefaultCacheAdapter implements ICacheAdapter {
|
||||
client: Client
|
||||
data: {
|
||||
[name: string]: Collection<string, any>
|
||||
} = {}
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
get(cacheName: string, key: string) {
|
||||
let cache = this.data[cacheName]
|
||||
if (!cache) return;
|
||||
return cache.get(key)
|
||||
}
|
||||
|
||||
set(cacheName: string, key: string, value: any) {
|
||||
let cache = this.data[cacheName]
|
||||
if (!cache) {
|
||||
this.data[cacheName] = new Collection()
|
||||
cache = this.data[cacheName]
|
||||
}
|
||||
cache.set(key, value)
|
||||
}
|
||||
|
||||
delete(cacheName: string, key: string) {
|
||||
let cache = this.data[cacheName]
|
||||
if (!cache) return false
|
||||
return cache.delete(key)
|
||||
}
|
||||
|
||||
array(cacheName: string) {
|
||||
let cache = this.data[cacheName]
|
||||
if (!cache) return
|
||||
return cache.array()
|
||||
}
|
||||
}
|
|
@ -1,30 +1,64 @@
|
|||
import { User } from '../structures/user.ts'
|
||||
import { GatewayIntents } from '../types/gatewayTypes.ts'
|
||||
import { Gateway } from '../gateway/index.ts'
|
||||
import { Rest } from './rest.ts'
|
||||
import { RESTManager } from './rest.ts'
|
||||
import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts'
|
||||
import { DefaultCacheAdapter, ICacheAdapter } from "./CacheAdapter.ts"
|
||||
import { UserManager } from "../managers/UsersManager.ts"
|
||||
import { GuildManager } from "../managers/GuildsManager.ts"
|
||||
import { EmojisManager } from "../managers/EmojisManager.ts"
|
||||
import { ChannelsManager } from "../managers/ChannelsManager.ts"
|
||||
|
||||
/** Some Client Options to modify behaviour */
|
||||
export interface ClientOptions {
|
||||
token?: string
|
||||
intents?: GatewayIntents[]
|
||||
cache?: ICacheAdapter
|
||||
}
|
||||
|
||||
/**
|
||||
* Discord Client.
|
||||
*/
|
||||
export class Client extends EventEmitter {
|
||||
gateway?: Gateway
|
||||
rest?: Rest
|
||||
rest: RESTManager = new RESTManager(this)
|
||||
user?: User
|
||||
ping = 0
|
||||
token?: string
|
||||
cache: ICacheAdapter = new DefaultCacheAdapter(this)
|
||||
intents?: GatewayIntents[]
|
||||
users: UserManager = new UserManager(this)
|
||||
guilds: GuildManager = new GuildManager(this)
|
||||
channels: ChannelsManager = new ChannelsManager(this)
|
||||
emojis: EmojisManager = new EmojisManager(this)
|
||||
|
||||
// constructor () {
|
||||
// super()
|
||||
// }
|
||||
constructor (options: ClientOptions = {}) {
|
||||
super()
|
||||
this.token = options.token
|
||||
this.intents = options.intents
|
||||
if(options.cache) this.cache = options.cache
|
||||
}
|
||||
|
||||
debug(tag: string, msg: string) {
|
||||
this.emit("debug", `[${tag}] ${msg}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used for connect to discord.
|
||||
* @param token Your token. This is required.
|
||||
* @param intents Gateway intents in array. This is required.
|
||||
*/
|
||||
connect (token: string, intents: GatewayIntents[]): void {
|
||||
this.token = token
|
||||
connect (token?: string, intents?: GatewayIntents[]): void {
|
||||
if(!token && this.token) token = this.token
|
||||
else if(!this.token && token) {
|
||||
this.token = token
|
||||
}
|
||||
else throw new Error("No Token Provided")
|
||||
if(!intents && this.intents) intents = this.intents
|
||||
else if(intents && !this.intents) {
|
||||
this.intents = intents
|
||||
}
|
||||
else throw new Error("No Gateway Intents were provided")
|
||||
this.gateway = new Gateway(this, token, intents)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,364 @@
|
|||
import { Client } from './client.ts'
|
||||
import { delay } from "../utils/index.ts";
|
||||
import * as baseEndpoints from "../consts/urlsAndVersions.ts";
|
||||
import { Client } from "./client.ts";
|
||||
|
||||
class Rest {
|
||||
client: Client
|
||||
constructor (client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
// TODO: make endpoints function
|
||||
export enum HttpResponseCode {
|
||||
Ok = 200,
|
||||
Created = 201,
|
||||
NoContent = 204,
|
||||
NotModified = 304,
|
||||
BadRequest = 400,
|
||||
Unauthorized = 401,
|
||||
Forbidden = 403,
|
||||
NotFound = 404,
|
||||
MethodNotAllowed = 405,
|
||||
TooManyRequests = 429,
|
||||
GatewayUnavailable = 502,
|
||||
// ServerError left untyped because it's 5xx.
|
||||
}
|
||||
|
||||
export { Rest }
|
||||
export type RequestMethods =
|
||||
| "get"
|
||||
| "post"
|
||||
| "put"
|
||||
| "patch"
|
||||
| "head"
|
||||
| "delete";
|
||||
|
||||
export interface QueuedRequest {
|
||||
callback: () => Promise<
|
||||
void | {
|
||||
rateLimited: any;
|
||||
beforeFetch: boolean;
|
||||
bucketID?: string | null;
|
||||
}
|
||||
>;
|
||||
bucketID?: string | null;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface RateLimitedPath {
|
||||
url: string;
|
||||
resetTimestamp: number;
|
||||
bucketID: string | null;
|
||||
}
|
||||
|
||||
export class RESTManager {
|
||||
client: Client;
|
||||
globallyRateLimited: boolean = false;
|
||||
queueInProcess: boolean = false;
|
||||
pathQueues: { [key: string]: QueuedRequest[] } = {};
|
||||
ratelimitedPaths = new Map<string, RateLimitedPath>();
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async processRateLimitedPaths() {
|
||||
const now = Date.now();
|
||||
this.ratelimitedPaths.forEach((value, key) => {
|
||||
if (value.resetTimestamp > now) return;
|
||||
this.ratelimitedPaths.delete(key);
|
||||
if (key === "global") this.globallyRateLimited = false;
|
||||
});
|
||||
|
||||
await delay(1000);
|
||||
this.processRateLimitedPaths();
|
||||
}
|
||||
|
||||
addToQueue(request: QueuedRequest) {
|
||||
const route = request.url.substring(baseEndpoints.DISCORD_API_URL.length + 1);
|
||||
const parts = route.split("/");
|
||||
// Remove the major param
|
||||
parts.shift();
|
||||
const [id] = parts;
|
||||
|
||||
if (this.pathQueues[id]) {
|
||||
this.pathQueues[id].push(request);
|
||||
} else {
|
||||
this.pathQueues[id] = [request];
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupQueues() {
|
||||
Object.entries(this.pathQueues).map(([key, value]) => {
|
||||
if (!value.length) {
|
||||
// Remove it entirely
|
||||
delete this.pathQueues[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async processQueue() {
|
||||
if (
|
||||
(Object.keys(this.pathQueues).length) && !this.globallyRateLimited
|
||||
) {
|
||||
await Promise.allSettled(
|
||||
Object.values(this.pathQueues).map(async (pathQueue) => {
|
||||
const request = pathQueue.shift();
|
||||
if (!request) return;
|
||||
|
||||
const rateLimitedURLResetIn = await this.checkRatelimits(request.url);
|
||||
|
||||
if (request.bucketID) {
|
||||
const rateLimitResetIn = await this.checkRatelimits(request.bucketID);
|
||||
if (rateLimitResetIn) {
|
||||
// This request is still rate limited readd to queue
|
||||
this.addToQueue(request);
|
||||
} else if (rateLimitedURLResetIn) {
|
||||
// This URL is rate limited readd to queue
|
||||
this.addToQueue(request);
|
||||
} else {
|
||||
// This request is not rate limited so it should be run
|
||||
const result = await request.callback();
|
||||
if (result && result.rateLimited) {
|
||||
this.addToQueue(
|
||||
{ ...request, bucketID: result.bucketID || request.bucketID },
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (rateLimitedURLResetIn) {
|
||||
// This URL is rate limited readd to queue
|
||||
this.addToQueue(request);
|
||||
} else {
|
||||
// This request has no bucket id so it should be processed
|
||||
const result = await request.callback();
|
||||
if (request && result && result.rateLimited) {
|
||||
this.addToQueue(
|
||||
{ ...request, bucketID: result.bucketID || request.bucketID },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(this.pathQueues).length) {
|
||||
await delay(1000);
|
||||
this.processQueue();
|
||||
this.cleanupQueues();
|
||||
} else this.queueInProcess = false;
|
||||
}
|
||||
|
||||
createRequestBody(body: any, method: RequestMethods) {
|
||||
const headers: { [key: string]: string } = {
|
||||
Authorization: `Bot ${this.client.token}`,
|
||||
"User-Agent":
|
||||
`DiscordBot (discord.deno)`,
|
||||
};
|
||||
|
||||
if(!this.client.token) delete headers["Authorization"];
|
||||
|
||||
if (method === "get") body = undefined;
|
||||
|
||||
if (body?.reason) {
|
||||
headers["X-Audit-Log-Reason"] = encodeURIComponent(body.reason);
|
||||
}
|
||||
|
||||
if (body?.file) {
|
||||
const form = new FormData();
|
||||
form.append("file", body.file.blob, body.file.name);
|
||||
form.append("payload_json", JSON.stringify({ ...body, file: undefined }));
|
||||
body.file = form;
|
||||
} else if (
|
||||
body && !["get", "delete"].includes(method)
|
||||
) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
body: body?.file || JSON.stringify(body),
|
||||
method: method.toUpperCase(),
|
||||
};
|
||||
}
|
||||
|
||||
async checkRatelimits(url: string) {
|
||||
const ratelimited = this.ratelimitedPaths.get(url);
|
||||
const global = this.ratelimitedPaths.get("global");
|
||||
const now = Date.now();
|
||||
|
||||
if (ratelimited && now < ratelimited.resetTimestamp) {
|
||||
return ratelimited.resetTimestamp - now;
|
||||
}
|
||||
if (global && now < global.resetTimestamp) {
|
||||
return global.resetTimestamp - now;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async runMethod(
|
||||
method: RequestMethods,
|
||||
url: string,
|
||||
body?: unknown,
|
||||
retryCount = 0,
|
||||
bucketID?: string | null,
|
||||
) {
|
||||
|
||||
const errorStack = new Error("Location In Your Files:");
|
||||
Error.captureStackTrace(errorStack);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = async () => {
|
||||
try {
|
||||
const rateLimitResetIn = await this.checkRatelimits(url);
|
||||
if (rateLimitResetIn) {
|
||||
return { rateLimited: rateLimitResetIn, beforeFetch: true, bucketID };
|
||||
}
|
||||
|
||||
const query = method === "get" && body
|
||||
? Object.entries(body as any).map(([key, value]) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value as any)}`
|
||||
)
|
||||
.join("&")
|
||||
: "";
|
||||
const urlToUse = method === "get" && query ? `${url}?${query}` : url;
|
||||
|
||||
const response = await fetch(urlToUse, this.createRequestBody(body, method));
|
||||
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();
|
||||
|
||||
const json = await response.json();
|
||||
if (
|
||||
json.retry_after ||
|
||||
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,
|
||||
});
|
||||
if (!this.queueInProcess) {
|
||||
this.queueInProcess = true;
|
||||
this.processQueue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async logErrors(response: Response, errorStack?: unknown) {
|
||||
try {
|
||||
const error = await response.json();
|
||||
console.error(error);
|
||||
} catch {
|
||||
console.error(response);
|
||||
}
|
||||
}
|
||||
|
||||
handleStatusCode(response: Response, errorStack?: unknown) {
|
||||
const status = response.status;
|
||||
|
||||
if (
|
||||
(status >= 200 && status < 400) ||
|
||||
status === HttpResponseCode.TooManyRequests
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.logErrors(response, errorStack);
|
||||
|
||||
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) {
|
||||
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 && remaining === "0") {
|
||||
ratelimited = true;
|
||||
|
||||
this.ratelimitedPaths.set(url, {
|
||||
url,
|
||||
resetTimestamp: Number(resetTimestamp) * 1000,
|
||||
bucketID,
|
||||
});
|
||||
|
||||
if (bucketID) {
|
||||
this.ratelimitedPaths.set(bucketID, {
|
||||
url,
|
||||
resetTimestamp: Number(resetTimestamp) * 1000,
|
||||
bucketID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no remaining global limit, we save it in cache
|
||||
if (global) {
|
||||
const reset = Date.now() + Number(retryAfter);
|
||||
this.globallyRateLimited = true;
|
||||
ratelimited = true;
|
||||
|
||||
this.ratelimitedPaths.set("global", {
|
||||
url: "global",
|
||||
resetTimestamp: reset,
|
||||
bucketID,
|
||||
});
|
||||
|
||||
if (bucketID) {
|
||||
this.ratelimitedPaths.set(bucketID, {
|
||||
url: "global",
|
||||
resetTimestamp: reset,
|
||||
bucketID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ratelimited ? bucketID : undefined;
|
||||
}
|
||||
|
||||
get(url: string, body?: unknown) {
|
||||
return this.runMethod("get", url, body);
|
||||
}
|
||||
post(url: string, body?: unknown) {
|
||||
return this.runMethod("post", url, body);
|
||||
}
|
||||
delete(url: string, body?: unknown) {
|
||||
return this.runMethod("delete", url, body);
|
||||
}
|
||||
patch(url: string, body?: unknown) {
|
||||
return this.runMethod("patch", url, body);
|
||||
}
|
||||
put(url: string, body?: unknown) {
|
||||
return this.runMethod("put", url, body);
|
||||
}
|
||||
}
|
|
@ -5,10 +5,11 @@ import { Base } from './base.ts'
|
|||
import { Channel } from './channel.ts'
|
||||
import { Emoji } from './emoji.ts'
|
||||
import { Member } from './member.ts'
|
||||
import { Role } from './role.ts'
|
||||
import { VoiceState } from './voiceState.ts'
|
||||
import cache from '../models/cache.ts'
|
||||
import getChannelByType from '../utils/getChannelByType.ts'
|
||||
import { RolesManager } from "../managers/RolesManager.ts"
|
||||
import { Role } from "./role.ts"
|
||||
|
||||
export class Guild extends Base {
|
||||
id: string
|
||||
|
@ -28,7 +29,7 @@ export class Guild extends Base {
|
|||
verificationLevel?: string
|
||||
defaultMessageNotifications?: string
|
||||
explicitContentFilter?: string
|
||||
roles?: Role[]
|
||||
roles: RolesManager = new RolesManager(this.client, this)
|
||||
emojis?: Emoji[]
|
||||
features?: GuildFeatures[]
|
||||
mfaLevel?: string
|
||||
|
@ -79,9 +80,12 @@ export class Guild extends Base {
|
|||
this.verificationLevel = data.verification_level
|
||||
this.defaultMessageNotifications = data.default_message_notifications
|
||||
this.explicitContentFilter = data.explicit_content_filter
|
||||
this.roles = data.roles.map(
|
||||
v => cache.get('role', v.id) ?? new Role(client, v)
|
||||
)
|
||||
// this.roles = data.roles.map(
|
||||
// v => cache.get('role', v.id) ?? new Role(client, v)
|
||||
// )
|
||||
data.roles.forEach(role => {
|
||||
this.roles.set(role.id, new Role(client, role))
|
||||
})
|
||||
this.emojis = data.emojis.map(
|
||||
v => cache.get('emoji', v.id) ?? new Emoji(client, v)
|
||||
)
|
||||
|
@ -120,7 +124,6 @@ export class Guild extends Base {
|
|||
this.approximateNumberCount = data.approximate_number_count
|
||||
this.approximatePresenceCount = data.approximate_presence_count
|
||||
}
|
||||
cache.set('guild', this.id, this)
|
||||
}
|
||||
|
||||
protected readFromData (data: GuildPayload): void {
|
||||
|
@ -147,10 +150,10 @@ export class Guild extends Base {
|
|||
data.default_message_notifications ?? this.defaultMessageNotifications
|
||||
this.explicitContentFilter =
|
||||
data.explicit_content_filter ?? this.explicitContentFilter
|
||||
this.roles =
|
||||
data.roles.map(
|
||||
v => cache.get('role', v.id) ?? new Role(this.client, v)
|
||||
) ?? this.roles
|
||||
// this.roles =
|
||||
// data.roles.map(
|
||||
// v => cache.get('role', v.id) ?? new Role(this.client, v)
|
||||
// ) ?? this.roles
|
||||
this.emojis =
|
||||
data.emojis.map(
|
||||
v => cache.get('emoji', v.id) ?? new Emoji(this.client, v)
|
||||
|
|
|
@ -18,6 +18,10 @@ export class User extends Base {
|
|||
premiumType?: 0 | 1 | 2
|
||||
publicFlags?: number
|
||||
|
||||
get tag(): string {
|
||||
return `${this.username}#${this.discriminator}`;
|
||||
}
|
||||
|
||||
get nickMention (): string {
|
||||
return `<@!${this.id}>`
|
||||
}
|
||||
|
@ -59,4 +63,8 @@ export class User extends Base {
|
|||
this.premiumType = data.premium_type ?? this.premiumType
|
||||
this.publicFlags = data.public_flags ?? this.publicFlags
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.mention;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
const TOKEN = ''
|
||||
export { TOKEN }
|
||||
export const TOKEN = ''
|
|
@ -10,9 +10,11 @@ import { User } from '../structures/user.ts'
|
|||
const bot = new Client()
|
||||
|
||||
bot.on('ready', () => {
|
||||
console.log('READY!')
|
||||
console.log(`[Login] Logged in as ${bot.user?.tag}!`)
|
||||
})
|
||||
|
||||
bot.on('debug', console.log)
|
||||
|
||||
bot.on('channelDelete', (channel: Channel) => {
|
||||
console.log('channelDelete', channel.id)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export interface ISessionStartLimit {
|
||||
total: number
|
||||
remaining: number
|
||||
reset_after: number
|
||||
}
|
||||
|
||||
export interface GatewayBotPayload {
|
||||
url: string
|
||||
shards: number
|
||||
session_start_limit: ISessionStartLimit
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
export class Collection<K, V> extends Map<K, V> {
|
||||
maxSize?: number;
|
||||
|
||||
set(key: K, value: V) {
|
||||
if (this.maxSize || this.maxSize === 0) {
|
||||
if (this.size >= this.maxSize) return this
|
||||
}
|
||||
|
||||
return super.set(key, value)
|
||||
}
|
||||
|
||||
array() {
|
||||
return [...this.values()]
|
||||
}
|
||||
|
||||
first(): V {
|
||||
return this.values().next().value
|
||||
}
|
||||
|
||||
last(): V {
|
||||
return [...this.values()][this.size - 1]
|
||||
}
|
||||
|
||||
random() {
|
||||
let arr = [...this.values()]
|
||||
return arr[Math.floor(Math.random() * arr.length)]
|
||||
}
|
||||
|
||||
find(callback: (value: V, key: K) => boolean) {
|
||||
for (const key of this.keys()) {
|
||||
const value = this.get(key)!
|
||||
if (callback(value, key)) return value
|
||||
}
|
||||
// If nothing matched
|
||||
return;
|
||||
}
|
||||
|
||||
filter(callback: (value: V, key: K) => boolean) {
|
||||
const relevant = new Collection<K, V>()
|
||||
this.forEach((value, key) => {
|
||||
if (callback(value, key)) relevant.set(key, value)
|
||||
});
|
||||
|
||||
return relevant;
|
||||
}
|
||||
|
||||
map<T>(callback: (value: V, key: K) => T) {
|
||||
const results = []
|
||||
for (const key of this.keys()) {
|
||||
const value = this.get(key)!
|
||||
results.push(callback(value, key))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
some(callback: (value: V, key: K) => boolean) {
|
||||
for (const key of this.keys()) {
|
||||
const value = this.get(key)!
|
||||
if (callback(value, key)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
every(callback: (value: V, key: K) => boolean) {
|
||||
for (const key of this.keys()) {
|
||||
const value = this.get(key)!
|
||||
if (!callback(value, key)) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
reduce<T>(
|
||||
callback: (accumulator: T, value: V, key: K) => T,
|
||||
initialValue?: T,
|
||||
): T {
|
||||
let accumulator: T = initialValue!
|
||||
|
||||
for (const key of this.keys()) {
|
||||
const value = this.get(key)!
|
||||
accumulator = callback(accumulator, value, key)
|
||||
}
|
||||
|
||||
return accumulator
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const delay = (ms: number) => new Promise((resolve, reject) => {
|
||||
setTimeout(() => resolve(true), ms);
|
||||
});
|
|
@ -1,3 +1,3 @@
|
|||
import getChannelByType from './getChannelByType.ts'
|
||||
|
||||
export default { getChannelByType }
|
||||
export { default as getChannelByType } from './getChannelByType.ts'
|
||||
export type AnyFunction<ReturnType = any> = (...args:any[]) => ReturnType;
|
||||
export { delay } from './delay.ts'
|
Loading…
Reference in New Issue