RESTManager, CacheAdapters, and improvements

This commit is contained in:
DjDeveloperr 2020-10-31 17:15:33 +05:30
parent f319e0df91
commit 935456906d
26 changed files with 770 additions and 57 deletions

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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')
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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))
})
}
}

View File

@ -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))
})
}
}

View File

@ -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))
})
}
}

View File

@ -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))
})
}
}

View File

@ -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))
})
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -1,2 +1 @@
const TOKEN = ''
export { TOKEN }
export const TOKEN = ''

View File

@ -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)
})

11
src/types/gatewayBot.ts Normal file
View File

@ -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
}

87
src/utils/collection.ts Normal file
View File

@ -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
}
}

3
src/utils/delay.ts Normal file
View File

@ -0,0 +1,3 @@
export const delay = (ms: number) => new Promise((resolve, reject) => {
setTimeout(() => resolve(true), ms);
});

View File

@ -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'