diff --git a/.gitignore b/.gitignore index 71f55de..f853be3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules/ # Build output build/ -package-lock.json \ No newline at end of file +package-lock.json +.vscode \ No newline at end of file diff --git a/package.json b/package.json index d67f6d5..ba83b21 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,18 @@ }, "homepage": "https://github.com/NotSugden/OrcusJS#readme", "dependencies": { - "@types/node": "^14.6.2", - "@types/node-fetch": "^2.5.7", + "@discordjs/collection": "^0.1.6", + "discord-api-types": "^0.4.1", "node-fetch": "^2.6.0", - "typescript": "^4.0.2" + "ws": "^7.3.1" }, "devDependencies": { + "@types/node": "^14.10.0", + "@types/node-fetch": "^2.5.7", + "@types/ws": "^7.2.6", "@typescript-eslint/eslint-plugin": "^3.10.1", "@typescript-eslint/parser": "^3.10.1", - "eslint": "^7.7.0" + "eslint": "^7.7.0", + "typescript": "^4.0.2" } -} \ No newline at end of file +} diff --git a/src/client/Client.ts b/src/client/Client.ts new file mode 100644 index 0000000..3effbb6 --- /dev/null +++ b/src/client/Client.ts @@ -0,0 +1,55 @@ +import { RequestManager } from './rest'; +import Util, { Constants } from '../util'; +import { EventEmitter } from 'events'; +import { buildRoute, Route } from './rest/buildRoute'; +import GatewayManager from './gateway/GatewayManager'; +import { UserCacheManager } from '../managers'; + +type Partialize = { + [K in keyof T]?: T[K] extends object ? Partialize : T[K]; +} + +export default class Client extends EventEmitter { + public gatewayManager: GatewayManager; + public options: ClientOptions; + public requestManager: RequestManager; + public token!: string | null; + public users: UserCacheManager; + + constructor(options: Partialize = {}) { + super(); + this.options = Util.mergeDefaults(options, Constants.DEFAULT_CLIENT_OPTIONS); + Object.defineProperty(this, + 'token', { + value: process.env.DISCORD_TOKEN ?? null, + writable: true + } + ); + this.gatewayManager = new GatewayManager(this); + this.requestManager = new RequestManager(this); + this.users = new UserCacheManager(this); + } + + /** + * @internal This is for internal use only + */ + public get api(): Route { + return buildRoute(this); + } + + public async login(token = this.token): Promise { + this.token = token; + await this.gatewayManager.connect(); + return this; + } +} + +export interface ClientOptions { + rest: { + requestOffset: number; + timeout: number | null; + }, + gateway: { + largeThreshold: number; + } +} \ No newline at end of file diff --git a/src/client/gateway/GatewayManager.ts b/src/client/gateway/GatewayManager.ts new file mode 100644 index 0000000..b24c3e6 --- /dev/null +++ b/src/client/gateway/GatewayManager.ts @@ -0,0 +1,241 @@ +import Client from '../Client'; +import * as WebSocket from 'ws'; +import { Error } from '../../errors'; +import { GATEWAY_VERSION, GatewayStatus } from '../../util/constants'; +import { TextDecoder } from 'util'; +import { EventEmitter } from 'events'; +import * as EventHandlers from './handlers'; +import { GatewayDispatchEvents, GatewayOPCodes, GatewayReadyDispatch, GatewayReceivePayload, GatewaySendPayload } from 'discord-api-types/v6'; + +const decoder = new TextDecoder(); + +const unpack = (data: WebSocket.Data) => { + if (data instanceof ArrayBuffer) data = new Uint8Array(data); + return JSON.parse( + typeof data === 'string' ? data : decoder.decode(data) + ); +}; + +export default class GatewayManager extends EventEmitter { + private _expectedGuilds: Set | null = null; + private _readyTimeout: NodeJS.Timeout | null = null; + private websocket: WebSocket | null = null; + private heartbeatInterval: NodeJS.Timeout | null = null; + private heartbeatSequence: number | null = null; + private lastPing: Date | null = null; + private lastAck: Date | null = null; + private sessionID: string | null = null; + + public client!: Client; + public status: GatewayStatus = GatewayStatus.NOT_CONNECTED; + + constructor(client: Client) { + super(); + Object.defineProperty(this, 'client', { value: client }); + } + + public get ping(): number | null { + if (!this.lastPing || !this.lastAck) return null; + return this.lastAck.getTime() - this.lastPing.getTime(); + } + + async connect(): Promise { + const { token } = this.client; + if (!token) { + throw new Error('MISSING_TOKEN'); + } + let url: string | null = null; + try { + const data = await this.client.api.gateway('bot').get<{ url: string }>(); + url = data.url; + } catch (error) { + if (error.httpStatus === 401) { + throw new Error('INVALID_TOKEN'); + } + throw error; + } + return new Promise((resolve, reject) => { + const removeListeners = () => { + this.off('ready', onReady); + this.off('disconnect', onClose); + }; + const onReady = () => { + this.status = GatewayStatus.CONNECTED; + this.client.emit('ready'); + resolve(); + removeListeners(); + }; + const onClose = () => { + if (this.status === GatewayStatus.RECONNECTING) return; + reject(); + removeListeners(); + }; + this.once('ready', onReady); + this.on('disconnect', onClose); + + const socket = this.websocket = new WebSocket(`${url}?v=${GATEWAY_VERSION}&encoding=json`); + socket.onerror = (error) => { + this.client.emit('gatewayError', error); + }; + socket.onopen = () => { + this.client.emit('debug', `Connecting to gateaway: ${url}`); + this.status = GatewayStatus.CONNECTING; + }; + socket.onclose = event => { + this.client.emit('debug', [ + 'Gateway WebSocket closed', + `Reason: ${event.reason || 'Unknown Reason'}`, + `Code: ${event.code}`, + `Was Clean: ${event.wasClean}` + ].join('\n')); + this.status = GatewayStatus.DISCONNECTED; + this.client.emit('gatewayDisconnect', event); + }; + socket.onmessage = async data => { + try { + await this.onMessage(data.data); + } catch (error) { + this.client.emit('gatewayError', error); + } + }; + }); + } + + public disconnect(code?: number): void { + if (!this.websocket) { + this.client.emit('warn', 'GatewayManager#disconnect was called before a connection was made.'); + return; + } + this.client.emit('debug', 'Gateaway disconnect'); + try { + this.websocket.close(code); + } catch { } // eslint-disable-line no-empty + if (this.heartbeatInterval) { + clearTimeout(this.heartbeatInterval); + this.heartbeatInterval = null; + } + this.status = GatewayStatus.DISCONNECTED; + this.emit('disconnect', code); + } + + public send(data: GatewaySendPayload): Promise { + if (!this.websocket) { + this.client.emit('warn', 'GatewayManager#send was called before a connection was made.'); + return Promise.resolve(); + } + return new Promise( + (resolve, reject) => this.websocket!.send(JSON.stringify(data), error => { + if (error) reject(error); + else resolve(); + }) + ); + } + + private async onMessage(data: WebSocket.Data) { + const packet: GatewayReceivePayload = unpack(data); + this.client.emit('gatewayPacket', packet); + if (packet.op === GatewayOPCodes.Dispatch) { + this.emit(packet.t, packet.d); + if (packet.t in EventHandlers) { + // Haven't added all the handlers yet so this will generate error but it will be fines + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + await EventHandlers[packet.t](this, packet.d); + } + if (packet.t === GatewayDispatchEvents.Ready) { + this._setupReady(packet.d); + } else if (packet.t === GatewayDispatchEvents.GuildCreate) { + if (this.status === GatewayStatus.WAITING_FOR_GUILDS) { + this._updateExpectedGuilds(packet.d.id); + } else { + this._expectedGuilds?.delete(packet.d.id); + if (this._expectedGuilds && !this._expectedGuilds.size) { + this._expectedGuilds = null; + } + } + } + } else if (packet.op === GatewayOPCodes.Heartbeat) { + this.heartbeatSequence = packet.d; + this.client.emit('debug', `Received heartbeat: ${packet.d} from the Gateway.`); + } else if (packet.op === GatewayOPCodes.Reconnect || packet.op === GatewayOPCodes.InvalidSession) { + if (packet.d) { + this.status = GatewayStatus.RECONNECTING; + } + this.disconnect(); + await this.reconnect(); + } else if (packet.op === GatewayOPCodes.HeartbeatAck) { + this.lastAck = new Date(); + } else if (packet.op === GatewayOPCodes.Hello) { + this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), packet.d.heartbeat_interval); + await this.identify(); + } + } + + private reconnect() { + this.status = GatewayStatus.RECONNECTING; + this.client.emit('debug', 'Reconnecting to gateway.'); + return this.connect(); + } + + private async sendHeartbeat() { + this.lastPing = new Date(); + try { + await this.send({ op: GatewayOPCodes.Heartbeat, d: this.heartbeatSequence || 0 }); + } catch (error) { + this.client.emit('gatewayError', error); + } + } + + private identify() { + return this.send(this.sessionID ? { + op: GatewayOPCodes.Resume, + d: { + seq: this.heartbeatSequence!, + session_id: this.sessionID, + token: this.client.token! + } + } : { + op: GatewayOPCodes.Identify, d: { + properties: { + $browser: 'OrcusJS', + // https://github.com/discordjs/discord-api-types/pull/17 incorrect type + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + $device: 'OrcusJS', + $os: process.platform + }, + token: this.client.token!, + large_threshold: this.client.options.gateway.largeThreshold + } + }); + } + + private _setupReady(data: GatewayReadyDispatch['d']) { + this._expectedGuilds = new Set(data.guilds.map(guild => guild.id)); + this.sessionID = data.session_id; + this.status = GatewayStatus.WAITING_FOR_GUILDS; + this._setReadyTimeout(); + } + + private _updateExpectedGuilds(id: string) { + this._expectedGuilds!.delete(id); + if (!this._expectedGuilds!.size) { + this._expectedGuilds = null; + clearTimeout(this._readyTimeout!); + this._readyTimeout = null; + this.emit('ready'); + } else { + this._setReadyTimeout(); + } + } + + private _setReadyTimeout() { + if (this._readyTimeout) { + clearTimeout(this._readyTimeout); + } + this._readyTimeout = setTimeout(() => { + this.emit('ready'); + this.client.emit('debug', `Ready emitted with missing guilds ${[...this._expectedGuilds!].join(', ')}`); + }, 15e3); + } +} diff --git a/src/client/gateway/handlers/GUILD_CREATE.ts b/src/client/gateway/handlers/GUILD_CREATE.ts new file mode 100644 index 0000000..21d3d4e --- /dev/null +++ b/src/client/gateway/handlers/GUILD_CREATE.ts @@ -0,0 +1,7 @@ +import { GatewayGuildCreateDispatch } from 'discord-api-types/v6'; +import GatewayManager from '../GatewayManager'; + +export default (manager: GatewayManager, data: GatewayGuildCreateDispatch): void => { + // const { client } = manager; + console.log('Recieved guild:', data); +}; \ No newline at end of file diff --git a/src/client/gateway/handlers/index.ts b/src/client/gateway/handlers/index.ts new file mode 100644 index 0000000..83e21a8 --- /dev/null +++ b/src/client/gateway/handlers/index.ts @@ -0,0 +1 @@ +export { default as GUILD_CREATE } from './GUILD_CREATE'; \ No newline at end of file diff --git a/src/client/gateway/index.ts b/src/client/gateway/index.ts new file mode 100644 index 0000000..9293ae9 --- /dev/null +++ b/src/client/gateway/index.ts @@ -0,0 +1,3 @@ +export { + default as GatewayManager +} from './GatewayManager'; \ No newline at end of file diff --git a/src/client/index.ts b/src/client/index.ts index 2d83a31..5fe4dd6 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,29 +1,6 @@ -import { RequestManager } from '../rest'; -import { Util, Constants } from '../util'; -import { EventEmitter } from 'events'; - -export class Client extends EventEmitter { - private requestManager: RequestManager; - - public token!: string | null; - public options: ClientOptions; - - constructor(options: Partial = {}) { - super(); - this.options = Util.mergeDefaults(options, Constants.DEFAULT_CLIENT_OPTIONS); - Object.defineProperty(this, - 'token', { - value: process.env.DISCORD_TOKEN ?? null, - writable: true - } - ); - this.requestManager = new RequestManager(this); - } -} - -export interface ClientOptions { - rest: { - requestOffset: number; - timeout: number | null; - } -} \ No newline at end of file +export { + default as Client, + ClientOptions +} from './Client'; +export * from './gateway'; +export * from './rest'; \ No newline at end of file diff --git a/src/rest/APIError.ts b/src/client/rest/APIError.ts similarity index 81% rename from src/rest/APIError.ts rename to src/client/rest/APIError.ts index 80ca9d1..9732193 100644 --- a/src/rest/APIError.ts +++ b/src/client/rest/APIError.ts @@ -12,14 +12,14 @@ export default class APIError extends Error { ) { super(); this.name = 'APIError'; - this.message = APIError.resolveError(data.errors); - this.code = data.code; + this.message = data.errors ? APIError.resolveError(data.errors) : data.message; + this.code = data.code; this.endpoint = endpoint; this.bucket = bucket; this.httpStatus = statusCode; } - static resolveError(rawError: RawAPIError['errors'], props: string[] = []): string { + static resolveError(rawError: Exclude, props: string[] = []): string { const messages: string[] = []; for (const [key, value] of Object.entries(rawError)) { const int = parseInt(key); @@ -42,7 +42,7 @@ interface BaseError { export interface RawAPIError { code: number; message: string; - errors: { + errors?: { [key: string]: { _errors: BaseError[]; } | { diff --git a/src/rest/HTTPError.ts b/src/client/rest/HTTPError.ts similarity index 100% rename from src/rest/HTTPError.ts rename to src/client/rest/HTTPError.ts diff --git a/src/rest/RequestManager.ts b/src/client/rest/RequestManager.ts similarity index 93% rename from src/rest/RequestManager.ts rename to src/client/rest/RequestManager.ts index 49f8728..7af6b54 100644 --- a/src/rest/RequestManager.ts +++ b/src/client/rest/RequestManager.ts @@ -1,9 +1,10 @@ -import { Client } from '../client'; +import Client from '../Client'; import { promisify } from 'util'; -import { Constants } from '../util'; import fetch, { Response } from 'node-fetch'; import APIError from './APIError'; import HTTPError from './HTTPError'; +import { Constants, RequestMethod } from '../../util'; +import { MethodRequestOptions } from './buildRoute'; const sleep = promisify(setTimeout); const parse = (response: Response) => { @@ -125,14 +126,10 @@ export interface RequestBucket { busy: boolean } -export interface RequestOptions { - data?: Record; - query?: Record | URLSearchParams; - reason?: string; + +export interface RequestOptions extends MethodRequestOptions { endpoint: string; bucket: string; resolve: (res: unknown) => void; reject: (error: Error) => void; -} - -export type RequestMethod = 'GET' | 'DELETE' | 'PATCH' | 'POST' \ No newline at end of file +} \ No newline at end of file diff --git a/src/client/rest/buildRoute.ts b/src/client/rest/buildRoute.ts new file mode 100644 index 0000000..8a41154 --- /dev/null +++ b/src/client/rest/buildRoute.ts @@ -0,0 +1,64 @@ +// inspired by discord.js https://github.com/discordjs/discord.js/blob/b0ab37ddc0614910e032ccf423816e106c3804e5/src/rest/APIRouter.js#L1 +import Client from '../Client'; +import { Constants, RequestMethod } from '../../util'; +import { RequestOptions } from './RequestManager'; + +const ID_REGEX = /^\d{16,19}$/; +const MAJOR_PARAMETERS = ['channels', 'guilds']; + +const getBucket = (route: string[]) => { + const bucket = []; + for (let index = 0;index < route.length;index++) { + const endpoint = route[index]; + if (index === 0) { + bucket.push(endpoint); + continue; + } + const previous = route[index - 1]; + if (previous === 'reactions') break; + const isMajor = ID_REGEX.test(endpoint) && MAJOR_PARAMETERS.includes(previous); + bucket.push(isMajor ? ':id' : endpoint); + } + return bucket; +}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + +export const buildRoute = (client: Client): Route => { + const route: string[] = []; + const handler: ProxyHandler = { + apply(_, __, args: string[]) { + route.push(...args.filter(arg => typeof arg === 'string')); + return new Proxy(noop, handler); + }, + + get(_, endpoint) { + endpoint = endpoint.toString(); + const uppercaseEndpoint = endpoint.toUpperCase(); + if (uppercaseEndpoint in Constants.RequestMethods) { + return (options?: MethodRequestOptions) => new Promise( + (resolve, reject) => client.requestManager.makeRequest( + uppercaseEndpoint, Object.assign({}, options || {}, { + bucket: getBucket(route).join('/'), + endpoint: route.join('/'), resolve, reject + }) + ) + ); + } + route.push(endpoint); + return new Proxy(noop, handler); + } + }; + return new Proxy(noop, handler); +}; + +export interface MethodRequestOptions { + data?: Record; + query?: Record | URLSearchParams; + reason?: string; +} + +export type Route = Record Route)> & { + get(options?: MethodRequestOptions): Promise +} \ No newline at end of file diff --git a/src/rest/index.ts b/src/client/rest/index.ts similarity index 100% rename from src/rest/index.ts rename to src/client/rest/index.ts diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..371ec8f --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,39 @@ +type Params = ( + (typeof messages)[T] extends ((...args: unknown[]) => string) + ? Parameters<(typeof messages)[T]> + : never[] +); + +const makeError = (Class: typeof Error) => { + return class OrcusError< + T extends keyof typeof messages + > extends Class { + public code: string; + constructor( + message: T, + ...params: Params + ) { + const msg = ) => string)> messages[message]; + super(typeof msg === 'function' ? msg(...params) : msg); + this.code = message; + } + + get name() { + return `${super.name} [${this.code}]`; + } + }; +}; + +const _Error = makeError(Error); +const _TypeError = makeError(TypeError); + +export { + _Error as Error, + _TypeError as TypeError +}; + +const messages = { + MISSING_TOKEN: 'The Client does not have a token attached.', + INVALID_TYPE: (parameter: string, expected: string) => `Supplied ${parameter} is not ${expected}`, + INVALID_TOKEN: 'An invalid token was provided' +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1ea90fe..54a2da4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ export { version } from '../package.json'; export * from './client'; -export * from './util'; -export * from './rest'; \ No newline at end of file +export * from './util'; \ No newline at end of file diff --git a/src/managers/BaseCacheManager.ts b/src/managers/BaseCacheManager.ts new file mode 100644 index 0000000..5cb67a0 --- /dev/null +++ b/src/managers/BaseCacheManager.ts @@ -0,0 +1,56 @@ +// Slightly inspired by Discord.JS's Manager system +// https://github.com/discordjs/discord.js/blob/1e63f3756e814d6b1a2a9c17af9c2b28ce37e472/src/managers/BaseManager.js + +import { FetchOptions } from '../util'; +import { Client } from '../client'; +import Base from '../structures/Base'; +import ValueCache from '../util/ValueCache'; + +type Constructable = new (client: Client, data: D) => T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ClassType = T extends new (...args: any[]) => infer R ? R : never + +export default abstract class BaseCacheManager< + Raw extends { id: string }, + Holds extends Constructable, Raw> +> { + protected readonly cacheType!: Holds; + protected readonly client!: Client; + + public cache: ValueCache>; + + protected constructor( + client: Client, cacheType: Holds + ) { + Object.defineProperties(this, { + cacheType: { value: cacheType }, + client: { value: client } + }); + this.cache = new ValueCache>(); + } + + /** + * @internal This method is for internal use only + */ + public add(data: Raw): ClassType { + const existing = this.cache.get(data.id); + if (existing) { + existing._update(data); + return existing; + } + const struct = > new this.cacheType(this.client, data); + this.cache.set(struct.id, struct); + return struct; + } + + /** + * @internal This method is for internal use only + */ + public remove(structOrId: ClassType | string): void { + this.cache.delete(typeof structOrId === 'string' ? structOrId : structOrId.id); + } + + public abstract fetch(struct: ClassType, options?: FetchOptions): Promise>; + public abstract fetch(id: string, options?: FetchOptions): Promise>; +} + diff --git a/src/managers/UserCacheManager.ts b/src/managers/UserCacheManager.ts new file mode 100644 index 0000000..50a5ee6 --- /dev/null +++ b/src/managers/UserCacheManager.ts @@ -0,0 +1,28 @@ +import { APIUser } from 'discord-api-types/v6'; +import { Client } from '../client'; +import { User } from '../structures'; +import { FetchOptions } from '../util'; +import BaseCacheManager from './BaseCacheManager'; + +export default class UserCacheManager extends BaseCacheManager { + constructor(client: Client) { + super(client, User); + } + + async fetch(userOrID: User | string, options: FetchOptions = {}): Promise { + let existing: User | null = null; + if (typeof userOrID === 'string') { + existing = this.cache.get(userOrID) || null; + if (existing && !options.force) return existing; + } else { + existing = userOrID; + userOrID = userOrID.id; + } + const data = await this.client.api.users(userOrID).get(); + if (existing && options.cache === false) { + existing._update(data); + return existing; + } + return this.add(data); + } +} \ No newline at end of file diff --git a/src/managers/index.ts b/src/managers/index.ts new file mode 100644 index 0000000..3c2164d --- /dev/null +++ b/src/managers/index.ts @@ -0,0 +1,2 @@ +export { default as BaseCacheManager } from './BaseCacheManager'; +export { default as UserCacheManager } from './UserCacheManager'; \ No newline at end of file diff --git a/src/rest/types/Application.ts b/src/rest/types/Application.ts deleted file mode 100644 index 15c7417..0000000 --- a/src/rest/types/Application.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Base } from './Generics'; -import { APIUser } from './User'; -import { TeamMembershipState } from '../../util/constants'; - -interface BaseApplication extends Base { - name: string; - icon: string | null; - description: string; - summary: string; -} - -export interface APIClientApplication extends BaseApplication { - rpc_origins?: string[]; - bot_public: boolean; - bot_require_code_grant: boolean; - owner: APIUser; - verify_key: string; - team: APITeam | null; - guild_id?: string; - primary_sku_id?: string; - slug?: string; - cover_image?: string; -} - -export interface APIIntegrationApplication extends BaseApplication { - bot?: APIUser; -} - -export interface APITeam extends Base { - icon: string | null; - owner_user_id: string; - members: APITeamMember[]; -} - -export interface APITeamMember { - permissions: ['*']; - team_id: string; - user: APIUser; - membership_state: TeamMembershipState; -} \ No newline at end of file diff --git a/src/rest/types/AuditLogEntry.ts b/src/rest/types/AuditLogEntry.ts deleted file mode 100644 index 3d841b1..0000000 --- a/src/rest/types/AuditLogEntry.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { APIOverwrite } from './Overwrite'; -import { AuditLogEvent } from '../../util/constants'; -import { PartialObject, Base } from './Generics'; - -export interface APIAuditLogEntry extends Base { - target_id: string; - changes?: APIAuditLogChange[]; - user_id: string; - action_type: AuditLogEvent; - options?: AuditLogEntryInfo; - reason?: string; -} - -export interface APIAuditLogChange { - new_value?: AuditLogChangeValue; - old_value?: AuditLogChangeValue; - key: string; -} - -export interface AuditLogEntryInfo { - delete_member_days?: string; - members_removed?: string; - channel_id?: string; - message_id?: string; - count?: string; - type?: string; - id?: string; - role_name?: string; -} - -export type AuditLogChangeValue = - | string - | number // Includes ChannelType - | boolean - | PartialObject[] // Role - | APIOverwrite[]; \ No newline at end of file diff --git a/src/rest/types/Channel.ts b/src/rest/types/Channel.ts deleted file mode 100644 index 42babe4..0000000 --- a/src/rest/types/Channel.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ChannelType } from '../../util/constants'; -import { APIOverwrite } from './Overwrite'; -import { APIUser } from './User'; -import { Base } from './Generics'; - -interface BaseChannel extends Base { - name: string; - type: ChannelType; -} - -interface TextBasedChannel extends BaseChannel { - last_message_id: string | null; - last_pin_timestamp: string; -} - -export interface APIGuildChannelBase extends BaseChannel { - guild_id: string; - position: number; - permission_overwrites: APIOverwrite[]; - parent_id: string | null; - nsfw: boolean; -} - -export interface APITextChannel extends APIGuildChannelBase, TextBasedChannel { - type: ChannelType.TEXT, - topic: string | null; - rate_limit_per_user: number; -} - -export interface APIDMChannel extends Omit { - type: ChannelType.DM, - recipients: [APIUser], -} - -export interface APIVoiceChannel extends APIGuildChannelBase { - type: ChannelType.VOICE; - nsfw: false; - bitrate: number; - user_limit: number; -} - -export interface APIGroupDMChannel extends Omit { - type: ChannelType.GROUP_DM; - name: null | string; - icon: null | string; -} - -export interface APICategoryChannel extends APIGuildChannelBase { - type: ChannelType.CATEGORY; - parent_id: null; - nsfw: false; -} - -export interface APINewsChannel extends Omit { - type: ChannelType.NEWS; -} - -export interface APIStoreChannel extends APIGuildChannelBase { - type: ChannelType.STORE; - nsfw: false; -} - -export interface APIChannelMention extends BaseChannel { - guild_id: string; -} - -// Group DM channel can only come from the `invites/{invite.code}` endpoint -export type APIChannel = - | APITextChannel - | APIDMChannel - | APIVoiceChannel - | APICategoryChannel - | APINewsChannel - | APICategoryChannel; - -export type APIGuildChannel = Exclude; \ No newline at end of file diff --git a/src/rest/types/Embed.ts b/src/rest/types/Embed.ts deleted file mode 100644 index bc4a3e6..0000000 --- a/src/rest/types/Embed.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { BaseAttachment } from './Message'; - -export interface APIEmbed { - title?: string; - type: EmbedType; - description?: string; - url?: string; - timestamp?: string; - color?: number; - footer?: APIEmbedFooter; - image?: APIEmbedImage; - thumbnail?: APIEmbedThumbnail; - video?: APIEmbedVideo; - provider?: APIEmbedProvider; - author?: APIEmbedAuthor; - fields?: APIEmbedField[]; -} - -export interface APIEmbedFooter { - text: string; - icon_url?: string; - proxy_icon_url?: string; -} - -export interface APIEmbedProvider { - name: string; - url: string; -} - -export interface APIEmbedAuthor { - name: string; - url?: string; - icon_url?: string; - proxy_icon_url?: string; -} - -export interface APIEmbedField { - name: string; - value: string; - inline: boolean; -} - -export type APIEmbedImage = BaseAttachment; - -export type APIEmbedThumbnail = BaseAttachment; - -export type APIEmbedVideo = Omit; - -export type EmbedType = 'rich' | 'image' | 'video' | 'gifv' | 'article' | 'link' \ No newline at end of file diff --git a/src/rest/types/Emoji.ts b/src/rest/types/Emoji.ts deleted file mode 100644 index b8b6d1f..0000000 --- a/src/rest/types/Emoji.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { APIUser } from './User'; -import { Base } from './Generics'; - -export interface APIUnicodeEmoji { - id: null; - name: string; -} - -export interface APIGuildEmoji extends Base { - name: string; - roles: string[]; - user: APIUser; - require_colons: boolean; - managed: boolean; - animated: boolean; - available: boolean; -} - -export type APIEmoji = APIUnicodeEmoji | APIGuildEmoji; \ No newline at end of file diff --git a/src/rest/types/Generics.ts b/src/rest/types/Generics.ts deleted file mode 100644 index dad5699..0000000 --- a/src/rest/types/Generics.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { APIUser } from './User'; -import { ChannelType } from '../../util/constants'; - -export interface Base { - id: string; -} - -export interface PartialObject extends Base { - name: string; -} - -export interface APIBan { - user: APIUser; - reason: string | null; -} - -export interface PartialAPIChannel extends Base { - name: string; - type: T; -} - -export interface APIVanityURL { - code: string | null; - uses: number; -} - -export interface APIGuildWidget { - enabled: boolean; - channel_id: string | null; -} \ No newline at end of file diff --git a/src/rest/types/Guild.ts b/src/rest/types/Guild.ts deleted file mode 100644 index a0b530f..0000000 --- a/src/rest/types/Guild.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Base } from './Generics'; -import { VerificationLevel, DefaultMessageNotification, ExplicitContentFilter, MFALevel, PremiumTier } from '../../util/constants'; -import { APIRole } from './Role'; -import { APIGuildEmoji } from './Emoji'; - -export interface APIGuildPreview extends Base { - name: string; - icon: string | null; - splash: string | null; - discovery_splash: string | null; - emojis: APIGuildEmoji[]; - features: GuildFeature[]; - description: string | null; - approximate_member_count: number; - approximate_presence_count: number; -} - -export interface APIGuild extends Omit< - APIGuildPreview, 'approximate_member_count' | 'approximate_presence_count' -> { - // true if the logged in user is the owner - owner?: true; - // permissions(_new) and owner only available through `users/@me/guilds` endpoint - permissions?: number; - permissions_new?: string; - owner_id: string; - region: string; - afk_channel_id: string | null; - afk_timeout: number; - // embed_* deprecated in favour of widget_* - embed_enabled: boolean; - embed_channel_id: string | null; - widget_enabled: boolean; - widget_channel_id: string | null; - verification_level: VerificationLevel; - default_message_notifications: DefaultMessageNotification; - explicit_content_filter: ExplicitContentFilter; - roles: APIRole[]; - mfa_level: MFALevel; - application_id: string | null; - system_channel_id: string | null; - system_channel_flags: number; - max_presences: number | null; - max_members: number; - vanity_url_code: string | null; - banner: string | null; - premium_tier: PremiumTier; - premium_subscription_count: number; - preferred_locale: string; - public_updates_channel_id: string | null; - max_video_channel_users: number; - // only if the `with_counts` parameter is true in `guilds/{guild.id}` endpoint - approximate_member_count?: number; - approximate_presence_count?: number; -} - -export type GuildFeature = - | 'INVITE_SPLASH' - | 'VIP_REGIONS' - | 'VANITY_URL' - | 'VERIFIED' - | 'PARTNERED' - | 'PUBLIC' - | 'COMMERCE' - | 'NEWS' - | 'DISCOVERABLE' - | 'FEATURABLE' - | 'ANIMATED_ICON' - | 'BANNER' - | 'PUBLIC_DISABLED' - | 'WELCOME_SCREEN_ENABLED'; - -export interface PartialAPIGuild { - id: string; - name: string; - splash: string | null; - banner: string | null; - description: string; - icon: string | null; - features: GuildFeature[]; - verification_level: VerificationLevel; - vanity_url_code: string | null; -} \ No newline at end of file diff --git a/src/rest/types/GuildMember.ts b/src/rest/types/GuildMember.ts deleted file mode 100644 index 94aba5d..0000000 --- a/src/rest/types/GuildMember.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { APIUser } from './User'; - -export interface APIGuildMember { - user: APIUser; - nick: string | null; - roles: string[]; - joined_at: string; - premium_since: string; - deaf: boolean; - mute: boolean; -} \ No newline at end of file diff --git a/src/rest/types/Integration.ts b/src/rest/types/Integration.ts deleted file mode 100644 index b2a58ab..0000000 --- a/src/rest/types/Integration.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Base } from './Generics'; -import { IntegrationType, IntegrationExpireBehavior } from '../../util/constants'; -import { APIUser } from './User'; -import { APIIntegrationApplication } from './Application'; - -interface BaseIntegration extends Base { - name: string; - type: IntegrationType; - enabled: boolean; - syncing: boolean; - role_id: string; - expire_behavior: IntegrationExpireBehavior; - expire_grace_period: number; - user?: APIUser; - account: APIIntegrationAccount; - synced_at: string; - subscriber_count: number; - revoked: boolean; -} - -export interface APIIntegrationAccount extends Base { - name: string; -} - -export interface APITwitchIntegration extends BaseIntegration { - enable_emoticons: boolean; -} - -export interface BotIntegration extends BaseIntegration { - application: APIIntegrationApplication -} \ No newline at end of file diff --git a/src/rest/types/Invite.ts b/src/rest/types/Invite.ts deleted file mode 100644 index 8bf34f1..0000000 --- a/src/rest/types/Invite.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { APIGroupDMChannel } from './Channel'; -import { PartialAPIChannel } from './Generics'; -import { ChannelType } from '../../util/constants'; -import { APIUser } from './User'; -import { PartialAPIGuild } from './Guild'; - -interface BaseInvite { - code: string; - channel: APIGroupDMChannel | PartialAPIChannel; -} - -export interface APIGroupDMInvite extends BaseInvite { - channel: APIGroupDMChannel; - inviter: APIUser; -} - -export interface APIGuildInvite extends BaseInvite { - guild: PartialAPIGuild; - channel: PartialAPIChannel; - inviter: APIUser; - // only present when `with_counts` is true on `/invites/{invite.code}` - approximate_member_count?: number; - approximate_presence_count?: number; -} - -export type APIVanityInvite = Omit - -export type APIInvite = APIVanityInvite | APIGuildInvite | APIGroupDMInvite; - -export interface APIInviteMeta extends APIVanityInvite { - inviter?: APIUser; - uses: number; - max_uses: number; - max_age: number; - temporary: boolean; - created_at: string; -} \ No newline at end of file diff --git a/src/rest/types/Message.ts b/src/rest/types/Message.ts deleted file mode 100644 index 1cc859f..0000000 --- a/src/rest/types/Message.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { APIUser } from './User'; -import { APIEmbed } from './Embed'; -import { APIEmoji } from './Emoji'; -import { MessageActivityType, MessageType } from '../../util/constants'; -import { APIChannelMention } from './Channel'; -import { Base } from './Generics'; - -interface BaseMessage extends Base { - channel_id: string; - author: APIUser; - content: string; - timestamp: string; - edited_timestamp: string | null; - tts: boolean; - mention_everyone: boolean; - mentions: APIUser[]; - mention_roles: string[]; - attachments: APIAttachment[]; - embeds: APIEmbed[]; - reactions?: APIMessageReaction[]; - pinned: boolean; - activity?: APIMessageActivity; - application?: APIMessageApplication; - flags?: number; -} - -export interface APIDMMessage extends BaseMessage { - type: MessageType.DEFAULT | MessageType.CHANNEL_PINNED_MESSAGE; - mention_everyone: false; - mention_roles: never[]; -} - -export interface APIGuildMessage extends BaseMessage { - type: MessageType; - guild_id: string; -} - -export interface APIWebhookMessage extends APIGuildMessage { - type: MessageType.DEFAULT; - webhook_id: string; -} - -export interface APICrosspostedMessage extends APIWebhookMessage { - mention_channels?: APIChannelMention[]; -} - -export type APIMessage = APICrosspostedMessage | APIWebhookMessage | APIGuildMessage | APIDMMessage; - -export interface BaseAttachment { - url: string; - proxy_url: string; - height: number; - width: number; -} - -export interface APIAttachment extends Omit { - id: string; - filename: string; - size: number; - height: number | null; - width: number | null; -} - -export interface APIMessageReaction { - count: number; - me: boolean; - emoji: APIEmoji; -} - -export interface APIMessageActivity { - type: MessageActivityType; - party_id?: string; -} - -export interface APIMessageApplication { - id: string; - cover_image?: string; - description: string; - icon: string | null; - name: string; -} \ No newline at end of file diff --git a/src/rest/types/Overwrite.ts b/src/rest/types/Overwrite.ts deleted file mode 100644 index 9b9d909..0000000 --- a/src/rest/types/Overwrite.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Base } from './Generics'; - -export interface APIOverwrite extends Base { - type: 'role' | 'member'; - // legacy - allow: number; - deny: number; - allow_new: string; - deny_new: string; -} - -export interface OverwriteData extends Base { - type: APIOverwrite['type']; - allow: string | number; - deny: string | number; -} \ No newline at end of file diff --git a/src/rest/types/Role.ts b/src/rest/types/Role.ts deleted file mode 100644 index 01b431f..0000000 --- a/src/rest/types/Role.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Base } from './Generics'; - -export interface APIRole extends Base { - name: string; - color: number; - hoist: boolean; - position: number; - // legacy - permissions: number; - permissions_new: string; - managed: boolean; - mentionable: boolean; -} \ No newline at end of file diff --git a/src/rest/types/User.ts b/src/rest/types/User.ts deleted file mode 100644 index 90025a9..0000000 --- a/src/rest/types/User.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Base } from './Generics'; - -export interface APIUser extends Base { - username: string; - avatar: string | null; - discriminator: string; - public_flags: number; - // bot is only present if true - bot?: true; -} \ No newline at end of file diff --git a/src/rest/types/Voice.ts b/src/rest/types/Voice.ts deleted file mode 100644 index 457c886..0000000 --- a/src/rest/types/Voice.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface APIVoiceRegion { - id: string; - name: string; - vip: boolean; - custom: boolean; - deprecated: boolean; - optimal: boolean; -} \ No newline at end of file diff --git a/src/rest/types/Webhook.ts b/src/rest/types/Webhook.ts deleted file mode 100644 index 5b881fe..0000000 --- a/src/rest/types/Webhook.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Base } from './Generics'; -import { WebhookType } from '../../util/constants'; -import { APIUser } from './User'; - -export interface APIWebhook extends Base { - type: WebhookType; - guild_id?: string; - channel_id: string; - user?: APIUser; - name: string | null; - avatar: string | null; - token?: string; -} \ No newline at end of file diff --git a/src/structures/Base.ts b/src/structures/Base.ts new file mode 100644 index 0000000..5ada303 --- /dev/null +++ b/src/structures/Base.ts @@ -0,0 +1,23 @@ +import { Client } from '../client'; + +export default abstract class Base { + protected readonly client!: Client; + + public readonly id!: string; + + constructor(client: Client, data: Raw) { + Object.defineProperties(this, { + client: { value: client }, + id: { enumerable: true, value: data.id, writable: false } + }); + + this._update(data); + } + + public abstract equals(data: Raw | Base): boolean; + + /** + * @internal This method is for internal use only + */ + public abstract _update(data: Raw): void +} \ No newline at end of file diff --git a/src/structures/User.ts b/src/structures/User.ts new file mode 100644 index 0000000..9aa4e63 --- /dev/null +++ b/src/structures/User.ts @@ -0,0 +1,88 @@ +import { APIUser } from 'discord-api-types/v6'; +import Util, { Constants, UserAvatarURLOptions } from '../util'; +import Base from './Base'; + +export default class User extends Base { + public avatarHash!: string | null; + public bot!: boolean; + public discriminator!: string; + // TODO: make class to handle this + public publicFlags!: number; + public system!: boolean; + public username!: string; + + public get defaultAvatarURL(): string { + return `${Constants.CDN_URL}/embed/avatars/${Number(this.discriminator) % 5}.png`; + } + + public avatarURL(options?: UserAvatarURLOptions & { fallbackDefault?: false }): string | null; + public avatarURL(options: UserAvatarURLOptions & { fallbackDefault: true }): string; + public avatarURL({ fallbackDefault = false, format, size }: UserAvatarURLOptions = {}): string | null { + if (!this.avatarHash) { + if (!fallbackDefault) return null; + return this.defaultAvatarURL; + } + let url = `${Constants.CDN_URL}/avatars/${this.id}/${this.avatarHash}.${ + typeof format === 'undefined' ? Util.resolveAvatarFormat(this.avatarHash) : format + }`; + if (typeof size === 'number') url += `?size=${size}`; + return url; + } + + public equals(data: APIUser | User): boolean { + const raw = data.id === this.id + && (data.bot || false) === this.bot + && data.discriminator === this.discriminator + && (data.system || false) === this.system + && data.username === this.username; + if (!raw) return false; + if ('avatar' in data) { + return data.avatar === this.avatarHash + && data.public_flags === this.publicFlags; + } else { + return data.avatarHash === this.avatarHash + && data.publicFlags === this.publicFlags; + } + } + + public fetch(): Promise { + return this.client.users.fetch(this, { force: true }); + } + + /** + * @internal Internal use only + */ + public _update(data: APIUser): void { + if ('avatar' in data) { + this.avatarHash = data.avatar; + } else if (typeof this.avatarHash === 'undefined') { + this.avatarHash = null; + } + + if ('bot' in data) { + this.bot = data.bot!; + } else if (typeof this.bot === 'undefined') { + // assume bot is false, API only sends `bot` if its true + this.bot = false; + } + + if ('discriminator' in data) { + this.discriminator = data.discriminator; + } + + if ('public_flags' in data) { + this.publicFlags = data.public_flags!; + } + + if ('system' in data) { + this.system = data.system!; + } else if (typeof this.system === 'undefined') { + // same as bot + this.system = false; + } + + if ('username' in data) { + this.username = data.username; + } + } +} \ No newline at end of file diff --git a/src/structures/index.ts b/src/structures/index.ts new file mode 100644 index 0000000..41c5de3 --- /dev/null +++ b/src/structures/index.ts @@ -0,0 +1,2 @@ +export { default as Base } from './Base'; +export { default as User } from './User'; \ No newline at end of file diff --git a/src/util/ValueCache.ts b/src/util/ValueCache.ts new file mode 100644 index 0000000..ba549d6 --- /dev/null +++ b/src/util/ValueCache.ts @@ -0,0 +1,59 @@ +// Slightly inspired by Discord.JS's collection package +// https://github.com/discordjs/collection +// Opted not to extend that as to use my own implementation for specific method + +export default class ValueCache extends Map { + public constructor(iterable?: Iterable | readonly (readonly [K, V])[]) { + // lazy type-fix + super(iterable!); + } + + public find(fn: (value: V, key: K, cache: this) => boolean): T | null { + for (const [key, value] of this) { + if (fn(value, key, this)) return value; + } + return null; + } + + public filter(fn: (value: V, key: K, cache: this) => boolean): ValueCache { + const filtered = new ValueCache(); + for (const [key, value] of this) { + if (fn(value, key, this)) filtered.set(key, value); + } + return filtered; + } + + /** + * Deletes values that meet the condition specified. + */ + public sweep(fn: (value: V, key: K, cache: this) => boolean): this { + for (const [key, value] of this) { + if (fn(value, key, this)) this.delete(key); + } + return this; + } + + /** + * Partitions the collection into multiple arrays. + * The return type for the callback should be the index of which that array + * should be in the returned array. + * use -1 to ignore value + * @example + * const [usersNamedJohn, usersNamedBob, usersNamedFred] = users.partition(value => { + * if (value.username === 'John') return 0; + * else if (value.username === 'Bob') return 1; + * else if (value.username === 'Fred') return 2; + * return -1; + * }) + */ + public partition(fn: (value: V, key: K, cache: this) => number): V[][] { + const arrays: V[][] = []; + for (const [key, value] of this) { + const index = fn(value, key, this); + if (index === -1) continue; + if (!arrays[index]) arrays[index] = [value]; + else arrays[index].push(value); + } + return arrays; + } +} \ No newline at end of file diff --git a/src/util/constants.ts b/src/util/constants.ts index 43b3347..a8f08bd 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -1,178 +1,44 @@ -export const API_URL = 'http://discord.com/api'; +import { ClientOptions } from '../client'; +import { GatewayDispatchEvents } from 'discord-api-types/v6'; + +export const API_URL = 'https://discord.com/api'; +export const CDN_URL = 'https://cdn.discordapp.com'; export const API_VERSION = 7; +export const GATEWAY_VERSION = 6; -export const DEFAULT_CLIENT_OPTIONS = { +export const DEFAULT_CLIENT_OPTIONS: ClientOptions = { rest: { requestOffset: 500, timeout: 20 * 1000 + }, + gateway: { + largeThreshold: 200 } }; -export enum AuditLogEvent { - GUILD_UPDATE = 1, - CHANNEL_CREATE = 10, - CHANNEL_UPDATE = 11, - CHANNEL_DELETE = 12, - CHANNEL_OVERWRITE_CREATE = 13, - CHANNEL_OVERWRITE_UPDATE = 14, - CHANNEL_OVERWRITE_DELETE = 15, - MEMBER_KICK = 20, - MEMBER_PRUNE = 21, - MEMBER_BAN_ADD = 22, - MEMBER_BAN_REMOVE = 23, - MEMBER_UPDATE = 24, - MEMBER_ROLE_UPDATE = 25, - MEMBER_MOVE = 26, - MEMBER_DISCONNECT = 27, - BOT_ADD = 28, - ROLE_CREATE = 30, - ROLE_UPDATE = 31, - ROLE_DELETE = 32, - INVITE_CREATE = 40, - INVITE_UPDATE = 41, - INVITE_DELETE = 42, - WEBHOOK_CREATE = 50, - WEBHOOK_UPDATE = 51, - WEBHOOK_DELETE = 52, - EMOJI_CREATE = 60, - EMOJI_UPDATE = 61, - EMOJI_DELETE = 62, - MESSAGE_DELETE = 72, - MESSAGE_BULK_DELETE = 73, - MESSAGE_PIN = 74, - MESSAGE_UNPIN = 75, - INTEGRATION_CREATE = 80, - INTEGRATION_UPDATE = 81, - INTEGRATION_DELETE = 82, -} - -export enum ChannelType { - TEXT = 0, - DM = 1, - VOICE = 2, - GROUP_DM = 3, - CATEGORY = 4, - NEWS = 5, - STORE = 6 -} - -// types 1-5 cannot be achieved on a bot account, thus not covered here -export enum MessageType { - DEFAULT = 0, - CHANNEL_PINNED_MESSAGE = 6, - GUILD_MEMBER_JOIN = 7, - USER_PREMIUM_GUILD_SUBSCRIPTION = 8, - USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9, - USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10, - USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11, - CHANNEL_FOLLOW_ADD = 12, - GUILD_DISCOVERY_DISQUALIFIED = 14, - GUILD_DISCOVERY_REQUALIFIED = 15, -} - -export enum MessageActivityType { - JOIN = 1, - SPECTATE = 2, - LISTEN = 3, - // Type 4 is undocumented, unknown - JOIN_REQUEST = 4 -} - -export enum MessageFlag { - CROSSPOSTED = 1 << 0, - IS_CROSSPOST = 1 << 2, - SUPPRESS_EMBEDS = 1 << 3, - SOURCE_MESSAGE_DELETED = 1 << 4, - URGENT = 1 << 5 -} - -export enum PermissionFlag { - CREATE_INSTANT_INVITE = 1 << 0, - KICK_MEMBERS = 1 << 1, - BAN_MEMBERS = 1 << 2, - ADMINISTRATOR = 1 << 3, - MANAGE_CHANNELS = 1 << 4, - MANAGE_GUILD = 1 << 5, - ADD_REACTIONS = 1 << 6, - VIEW_AUDIT_LOG = 1 << 7, - PRIORITY_SPEAKER = 1 << 8, - STREAM = 1 << 9, - VIEW_CHANNEL = 1 << 10, - SEND_MESSAGES = 1 << 11, - SEND_TTS_MESSAGES = 1 << 12, - MANAGE_MESSAGES = 1 << 13, - EMBED_LINKS = 1 << 14, - ATTACH_FILES = 1 << 15, - READ_MESSAGE_HISTORY = 1 << 16, - MENTION_EVERYONE = 1 << 17, - USE_EXTERNAL_EMOJIS = 1 << 18, - VIEW_GUILD_INSIGHTS = 1 << 19, - CONNECT = 1 << 20, - SPEAK = 1 << 21, - MUTE_MEMBERS = 1 << 22, - DEAFEN_MEMBERS = 1 << 23, - MOVE_MEMBERS = 1 << 24, - USE_VAD = 1 << 25, - CHANGE_NICKNAME = 1 << 26, - MANAGE_NICKNAMES = 1 << 27, - MANAGE_ROLES = 1 << 28, - MANAGE_WEBHOOKS = 1 << 29, - MANAGE_EMOJIS = 1 << 30, -} - -export enum VerificationLevel { - NONE = 0, - LOW = 1, - MEDIUM = 2, - HIGH = 3, - VERY_HIGH = 4 -} - -export enum DefaultMessageNotification { - ALL_MESSAGES = 0, - ONLY_MENTIONS = 1 -} - -export enum ExplicitContentFilter { - DISABLED = 0, - MEMBERS_WITHOUT_ROLES = 1, - ALL_MEMBERS = 2 -} - -export enum MFALevel { - NONE = 0, - ELEVATED = 1 -} - -export enum SystemChannelFlag { - SUPPRESS_JOIN_NOTIFICATIONS = 1 << 0, - SUPPRESS_PREMIUM_NOTIFICATIONS = 1 << 1 -} - -export enum PremiumTier { - NONE = 0, - TIER_1 = 1, - TIER_2 = 2, - TIER_3 = 3 -} +const mirror = (array: T[]) => { + const obj = > {}; + for (const key of array) obj[key] = key; + return obj; +}; -export enum IntegrationType { - YOUTUBE = 'youtube', - TWITCH = 'twitch', - DISCORD = 'discord' -} +export const RequestMethods = mirror(['GET', 'DELETE', 'PATCH', 'PUT', 'POST']); -export enum IntegrationExpireBehavior { - REMOVE_ROLE = 0, - KICK = 1 +export enum GatewayStatus { + NOT_CONNECTED = 0, + CONNECTING = 1, + WAITING_FOR_GUILDS = 2, + DISCONNECTED = 3, + RECONNECTING = 4, + CONNECTED = 5 } -export enum TeamMembershipState { - INVITED = 1, - ACCEPTED = 2 -} +export const WSEvents = mirror(Object.values(GatewayDispatchEvents)); -export enum WebhookType { - INCOMING = 1, - CHANNEL_FOLLOWER = 2 +export enum ImageFormats { + WEBP = 'webp', + PNG = 'png', + JPG = 'jpg', + JPEG = 'jpeg', + GIF = 'gif' } \ No newline at end of file diff --git a/src/util/index.ts b/src/util/index.ts index 25bc4cc..fc8a600 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,7 +1,16 @@ export * as Constants from './constants'; +export * from './types'; -export class Util { - static mergeDefaults(toMerge: Partial, defaults: T): T { +type Partialize = { + [K in keyof T]?: T[K] extends object ? Partialize : T[K]; +} + +export default class Util { + static resolveAvatarFormat(hash: string): string { + return hash.startsWith('a_') ? 'gif' : 'webp'; + } + + static mergeDefaults(toMerge: Partialize, defaults: T): T { for (const [key, _default] of Object.entries(defaults)) { const typeofDefault = typeof _default; const typeofGiven = typeof toMerge[ key]; diff --git a/src/util/types.ts b/src/util/types.ts new file mode 100644 index 0000000..6244367 --- /dev/null +++ b/src/util/types.ts @@ -0,0 +1,19 @@ +import { ImageFormats, RequestMethods } from './constants'; + +export interface FetchOptions { + cache?: boolean; + force?: boolean; +} + +export interface ImageURLOptions { + format?: ImageFormats; + size?: ImageSize; +} + +export interface UserAvatarURLOptions extends ImageURLOptions { + fallbackDefault?: boolean; +} + +export type ImageSize = 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096; + +export type RequestMethod = keyof typeof RequestMethods; \ No newline at end of file