collectors, rest options and all that

This commit is contained in:
DjDeveloperr 2021-01-07 19:16:56 +05:30
parent e9f461fef4
commit 48976e779b
9 changed files with 381 additions and 39 deletions

4
mod.ts
View file

@ -4,7 +4,9 @@ export { Gateway } from './src/gateway/index.ts'
export type { ClientEvents } from './src/gateway/handlers/index.ts' export type { ClientEvents } from './src/gateway/handlers/index.ts'
export * from './src/models/client.ts' export * from './src/models/client.ts'
export * from './src/models/slashClient.ts' export * from './src/models/slashClient.ts'
export { RESTManager } from './src/models/rest.ts' export { RESTManager, TokenType, HttpResponseCode } from './src/models/rest.ts'
export type { RequestHeaders } from './src/models/rest.ts'
export type { RESTOptions } from './src/models/rest.ts'
export * from './src/models/cacheAdapter.ts' export * from './src/models/cacheAdapter.ts'
export { export {
Command, Command,

View file

@ -348,4 +348,11 @@ export interface ClientEvents {
* @param message Debug message * @param message Debug message
*/ */
debug: [message: string] debug: [message: string]
/**
* Raw event which gives you access to raw events DISPATCH'd from Gateway
* @param evt Event name string
* @param payload Payload JSON of the event
*/
raw: [evt: string, payload: any]
} }

View file

@ -2,7 +2,7 @@
import { User } from '../structures/user.ts' import { User } from '../structures/user.ts'
import { GatewayIntents } from '../types/gateway.ts' import { GatewayIntents } from '../types/gateway.ts'
import { Gateway } from '../gateway/index.ts' import { Gateway } from '../gateway/index.ts'
import { RESTManager } from './rest.ts' import { RESTManager, RESTOptions, TokenType } from './rest.ts'
import { EventEmitter } from '../../deps.ts' import { EventEmitter } from '../../deps.ts'
import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts' import { DefaultCacheAdapter, ICacheAdapter } from './cacheAdapter.ts'
import { UsersManager } from '../managers/users.ts' import { UsersManager } from '../managers/users.ts'
@ -20,6 +20,7 @@ import { Application } from '../structures/application.ts'
import { Invite } from '../structures/invite.ts' import { Invite } from '../structures/invite.ts'
import { INVITE } from '../types/endpoint.ts' import { INVITE } from '../types/endpoint.ts'
import { ClientEvents } from '../gateway/handlers/index.ts' import { ClientEvents } from '../gateway/handlers/index.ts'
import type { Collector } from './collectors.ts'
/** OS related properties sent with Gateway Identify */ /** OS related properties sent with Gateway Identify */
export interface ClientProperties { export interface ClientProperties {
@ -54,6 +55,10 @@ export interface ClientOptions {
clientProperties?: ClientProperties clientProperties?: ClientProperties
/** Enable/Disable Slash Commands Integration (enabled by default) */ /** Enable/Disable Slash Commands Integration (enabled by default) */
enableSlash?: boolean enableSlash?: boolean
/** Disable taking token from env if not provided (token is taken from env if present by default) */
disableEnvToken?: boolean
/** Override REST Options */
restOptions?: RESTOptions
} }
export declare interface Client { export declare interface Client {
@ -89,7 +94,7 @@ export class Client extends EventEmitter {
/** Gateway object */ /** Gateway object */
gateway?: Gateway gateway?: Gateway
/** REST Manager - used to make all requests */ /** REST Manager - used to make all requests */
rest: RESTManager = new RESTManager(this) rest: RESTManager
/** User which Client logs in to, undefined until logs in */ /** User which Client logs in to, undefined until logs in */
user?: User user?: User
/** WebSocket ping of Client */ /** WebSocket ping of Client */
@ -118,8 +123,6 @@ export class Client extends EventEmitter {
channels: ChannelsManager = new ChannelsManager(this) channels: ChannelsManager = new ChannelsManager(this)
emojis: EmojisManager = new EmojisManager(this) emojis: EmojisManager = new EmojisManager(this)
/** Whether the REST Manager will use Canary API or not */
canary: boolean = false
/** Client's presence. Startup one if set before connecting */ /** Client's presence. Startup one if set before connecting */
presence: ClientPresence = new ClientPresence() presence: ClientPresence = new ClientPresence()
_decoratedEvents?: { _decoratedEvents?: {
@ -140,6 +143,7 @@ export class Client extends EventEmitter {
shard: number = 0 shard: number = 0
/** Shard Manager of this Client if Sharded */ /** Shard Manager of this Client if Sharded */
shardManager?: ShardManager shardManager?: ShardManager
collectors: Set<Collector> = new Set()
constructor(options: ClientOptions = {}) { constructor(options: ClientOptions = {}) {
super() super()
@ -153,7 +157,6 @@ export class Client extends EventEmitter {
options.presence instanceof ClientPresence options.presence instanceof ClientPresence
? options.presence ? options.presence
: new ClientPresence(options.presence) : new ClientPresence(options.presence)
if (options.canary === true) this.canary = true
if (options.messageCacheLifetime !== undefined) if (options.messageCacheLifetime !== undefined)
this.messageCacheLifetime = options.messageCacheLifetime this.messageCacheLifetime = options.messageCacheLifetime
if (options.reactionCacheLifetime !== undefined) if (options.reactionCacheLifetime !== undefined)
@ -185,6 +188,27 @@ export class Client extends EventEmitter {
client: this, client: this,
enabled: options.enableSlash enabled: options.enableSlash
}) })
if (this.token === undefined) {
try {
const token = Deno.env.get('DISCORD_TOKEN')
if (token !== undefined) {
this.token = token
this.debug('Info', 'Found token in ENV')
}
} catch (e) {}
}
const restOptions: RESTOptions = {
token: () => this.token,
tokenType: TokenType.Bot,
canary: options.canary,
client: this
}
if (options.restOptions !== undefined)
Object.assign(restOptions, options.restOptions)
this.rest = new RESTManager(restOptions)
} }
/** /**
@ -244,8 +268,8 @@ export class Client extends EventEmitter {
/** /**
* This function is used for connecting to discord. * This function is used for connecting to discord.
* @param token Your token. This is required. * @param token Your token. This is required if not given in ClientOptions.
* @param intents Gateway intents in array. This is required. * @param intents Gateway intents in array. This is required if not given in ClientOptions.
*/ */
connect(token?: string, intents?: GatewayIntents[]): void { connect(token?: string, intents?: GatewayIntents[]): void {
if (token === undefined && this.token !== undefined) token = this.token if (token === undefined && this.token !== undefined) token = this.token
@ -262,9 +286,40 @@ export class Client extends EventEmitter {
} else if (intents !== undefined && this.intents === undefined) { } else if (intents !== undefined && this.intents === undefined) {
this.intents = intents this.intents = intents
} else throw new Error('No Gateway Intents were provided') } else throw new Error('No Gateway Intents were provided')
this.rest.token = token
this.gateway = new Gateway(this, token, intents) this.gateway = new Gateway(this, token, intents)
} }
/** Add a new Collector */
addCollector(collector: Collector): boolean {
if (this.collectors.has(collector)) return false
else {
this.collectors.add(collector)
return true
}
}
/** Remove a Collector */
removeCollector(collector: Collector): boolean {
if (!this.collectors.has(collector)) return false
else {
this.collectors.delete(collector)
return true
}
}
emit(event: keyof ClientEvents, ...args: any[]): boolean {
const collectors: Collector[] = []
for (const collector of this.collectors.values()) {
if (collector.event === event) collectors.push(collector)
}
if (collectors.length !== 0) {
this.collectors.forEach((collector) => collector._fire(...args))
}
return super.emit(event, ...args)
}
/** Wait for an Event (optionally satisfying an event) to occur */ /** Wait for an Event (optionally satisfying an event) to occur */
async waitFor<K extends keyof ClientEvents>( async waitFor<K extends keyof ClientEvents>(
event: K, event: K,
@ -293,10 +348,13 @@ export class Client extends EventEmitter {
/** Event decorator to create an Event handler from function */ /** Event decorator to create an Event handler from function */
export function event(name?: keyof ClientEvents) { export function event(name?: keyof ClientEvents) {
return function (client: Client | Extension, prop: keyof ClientEvents) { return function (
client: Client | Extension,
prop: keyof ClientEvents | string
) {
const listener = ((client as unknown) as { const listener = ((client as unknown) as {
[name in keyof ClientEvents]: (...args: ClientEvents[name]) => any [name in keyof ClientEvents]: (...args: ClientEvents[name]) => any
})[prop] })[name ?? ((prop as unknown) as keyof ClientEvents)]
if (typeof listener !== 'function') if (typeof listener !== 'function')
throw new Error('@event decorator requires a function') throw new Error('@event decorator requires a function')
if (client._decoratedEvents === undefined) client._decoratedEvents = {} if (client._decoratedEvents === undefined) client._decoratedEvents = {}

162
src/models/collectors.ts Normal file
View file

@ -0,0 +1,162 @@
import { Collection } from '../utils/collection.ts'
import { EventEmitter } from '../../deps.ts'
import type { Client } from './client.ts'
export type CollectorFilter = (...args: any[]) => boolean | Promise<boolean>
export interface CollectorOptions {
/** Event name to listen for */
event: string
/** Optionally Client object for deinitOnEnd functionality */
client?: Client
/** Filter function */
filter?: CollectorFilter
/** Max entries to collect */
max?: number
/** Whether or not to de-initialize on end */
deinitOnEnd?: boolean
/** Timeout to end the Collector if not fulfilled if any filter or max */
timeout?: number
}
export class Collector extends EventEmitter {
client?: Client
private _started: boolean = false
event: string
filter: CollectorFilter = () => true
collected: Collection<string, any[]> = new Collection()
max?: number
deinitOnEnd: boolean = false
timeout?: number
private _timer?: number
get started(): boolean {
return this._started
}
set started(d: boolean) {
if (d !== this._started) {
this._started = d
if (d) this.emit('start')
else {
if (this.deinitOnEnd && this.client !== undefined)
this.deinit(this.client)
this.emit('end')
}
}
}
constructor(options: CollectorOptions | string) {
super()
if (typeof options === 'string') this.event = options
else {
this.event = options.event
this.client = options.client
this.filter = options.filter ?? (() => true)
this.max = options.max
this.deinitOnEnd = options.deinitOnEnd ?? false
this.timeout = options.timeout
}
}
/** Start collecting */
collect(): Collector {
this.started = true
if (this.client !== undefined) this.init(this.client)
if (this._timer !== undefined) clearTimeout(this._timer)
if (this.timeout !== undefined) {
this._timer = setTimeout(() => {
this.end()
}, this.timeout)
}
return this
}
/** End collecting */
end(): Collector {
this.started = false
if (this._timer !== undefined) clearTimeout(this._timer)
return this
}
/** Reset collector and start again */
reset(): Collector {
this.collected = new Collection()
this.collect()
return this
}
/** Init the Collector on Client */
init(client: Client): Collector {
this.client = client
client.addCollector(this)
return this
}
/** De initialize the Collector i.e. remove cleanly */
deinit(client: Client): Collector {
client.removeCollector(this)
return this
}
/** Checks we may want to perform on an extended version of Collector */
protected check(..._args: any[]): boolean | Promise<boolean> {
return true
}
/** Fire the Collector */
async _fire(...args: any[]): Promise<void> {
if (!this.started) return
const check = await this.check(...args)
if (!check) return
const filter = await this.filter(...args)
if (!filter) return
this.collected.set((Number(this.collected.size) + 1).toString(), args)
this.emit('collect', ...args)
if (
this.max !== undefined &&
// linter: send help
this.max < Number(this.collected.size) + 1
) {
this.end()
}
}
/** Set filter of the Collector */
when(filter: CollectorFilter): Collector {
this.filter = filter
return this
}
/** Add a new listener for 'collect' event */
each(handler: CallableFunction): Collector {
this.on('collect', () => handler())
return this
}
/** Returns a Promise resolved when Collector ends or a timeout occurs */
async wait(timeout: number = this.timeout ?? 0): Promise<Collector> {
return await new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (!timeout)
throw new Error(
'Timeout is required parameter if not given in CollectorOptions'
)
let done = false
const onend = (): void => {
done = true
this.removeListener('end', onend)
resolve(this)
}
this.on('end', onend)
setTimeout(() => {
if (!done) {
this.removeListener('end', onend)
reject(new Error('Timeout'))
}
}, timeout)
})
}
}

View file

@ -1,5 +1,6 @@
import * as baseEndpoints from '../consts/urlsAndVersions.ts' import * as baseEndpoints from '../consts/urlsAndVersions.ts'
import { Collection } from '../utils/collection.ts' import { Collection } from '../utils/collection.ts'
import { Client } from './client.ts'
export type RequestMethods = export type RequestMethods =
| 'get' | 'get'
@ -60,15 +61,23 @@ export type MethodFunction = (
) => Promise<any> ) => Promise<any>
export interface APIMap extends MethodFunction { export interface APIMap extends MethodFunction {
/** Make a GET request to current route */
get: APIMap get: APIMap
/** Make a POST request to current route */
post: APIMap post: APIMap
/** Make a PATCH request to current route */
patch: APIMap patch: APIMap
/** Make a PUT request to current route */
put: APIMap put: APIMap
/** Make a DELETE request to current route */
delete: APIMap delete: APIMap
/** Make a HEAD request to current route */
head: APIMap head: APIMap
/** Continue building API Route */
[name: string]: APIMap [name: string]: APIMap
} }
/** API Route builder function */
export const builder = (rest: RESTManager, acum = '/'): APIMap => { export const builder = (rest: RESTManager, acum = '/'): APIMap => {
const routes = {} const routes = {}
const proxy = new Proxy(routes, { const proxy = new Proxy(routes, {
@ -94,25 +103,76 @@ export const builder = (rest: RESTManager, acum = '/'): APIMap => {
} }
export interface RESTOptions { export interface RESTOptions {
token?: string /** Token to use for authorization */
token?: string | (() => string | undefined)
/** Headers to patch with if any */
headers?: { [name: string]: string | undefined } headers?: { [name: string]: string | undefined }
/** Whether to use Canary instance of Discord API or not */
canary?: boolean canary?: boolean
/** Discord REST API version to use */
version?: 6 | 7 | 8 version?: 6 | 7 | 8
/** Token Type to use for Authorization */
tokenType?: TokenType
/** User Agent to use (Header) */
userAgent?: string
/** Optional Harmony client */
client?: Client
} }
/** Token Type for REST API. */
export enum TokenType {
/** Token type for Bot User */
Bot = 'Bot',
/** Token Type for OAuth2 */
Bearer = 'Bearer',
/** No Token Type. Can be used for User accounts. */
None = ''
}
/** An easier to use interface for interacting with Discord REST API. */
export class RESTManager { export class RESTManager {
client?: RESTOptions
queues: { [key: string]: QueuedItem[] } = {} queues: { [key: string]: QueuedItem[] } = {}
rateLimits = new Collection<string, RateLimit>() rateLimits = new Collection<string, RateLimit>()
/** Whether we are globally ratelimited or not */
globalRateLimit: boolean = false globalRateLimit: boolean = false
/** Whether requests are being processed or not */
processing: boolean = false processing: boolean = false
/** API Version being used by REST Manager */
version: number = 8 version: number = 8
/**
* API Map - easy to use way for interacting with Discord API.
*
* Examples:
* * ```ts
* rest.api.users['123'].get().then(userPayload => doSomething)
* ```
* * ```ts
* rest.api.guilds['123'].channels.post({ name: 'my-channel', type: 0 }).then(channelPayload => {})
* ```
*/
api: APIMap api: APIMap
/** Token being used for Authorization */
token?: string | (() => string | undefined)
/** Token Type of the Token if any */
tokenType: TokenType = TokenType.Bot
/** Headers object which patch the current ones */
headers: any = {}
/** Optional custom User Agent (header) */
userAgent?: string
/** Whether REST Manager is using Canary API */
canary?: boolean
/** Optional Harmony Client object */
client?: Client
constructor(client?: RESTOptions) { constructor(options?: RESTOptions) {
this.client = client
this.api = builder(this) this.api = builder(this)
if (client?.version !== undefined) this.version = client.version if (options?.token !== undefined) this.token = options.token
if (options?.version !== undefined) this.version = options.version
if (options?.headers !== undefined) this.headers = options.headers
if (options?.tokenType !== undefined) this.tokenType = options.tokenType
if (options?.userAgent !== undefined) this.userAgent = options.userAgent
if (options?.canary !== undefined) this.canary = options.canary
if (options?.client !== undefined) this.client = options.client
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.handleRateLimits() this.handleRateLimits()
} }
@ -193,13 +253,16 @@ export class RESTManager {
private prepare(body: any, method: RequestMethods): { [key: string]: any } { private prepare(body: any, method: RequestMethods): { [key: string]: any } {
const headers: RequestHeaders = { const headers: RequestHeaders = {
'User-Agent': `DiscordBot (harmony, https://github.com/harmony-org/harmony)` 'User-Agent':
this.userAgent ??
`DiscordBot (harmony, https://github.com/harmony-org/harmony)`
} }
if (this.client !== undefined) if (this.token !== undefined) {
headers.Authorization = `Bot ${this.client.token}` const token = typeof this.token === 'string' ? this.token : this.token()
if (token !== undefined)
if (this.client?.token === undefined) delete headers.Authorization headers.Authorization = `${this.tokenType} ${token}`.trim()
}
if (method === 'get' || method === 'head' || method === 'delete') if (method === 'get' || method === 'head' || method === 'delete')
body = undefined body = undefined
@ -220,9 +283,7 @@ export class RESTManager {
headers['Content-Type'] = 'application/json' headers['Content-Type'] = 'application/json'
} }
if (this.client?.headers !== undefined) if (this.headers !== undefined) Object.assign(headers, this.headers)
Object.assign(headers, this.client.headers)
const data: { [name: string]: any } = { const data: { [name: string]: any } = {
headers, headers,
body: body?.file ?? JSON.stringify(body), body: body?.file ?? JSON.stringify(body),
@ -305,13 +366,25 @@ export class RESTManager {
body: any, body: any,
data: { [key: string]: any }, data: { [key: string]: any },
reject: CallableFunction reject: CallableFunction
): Promise<undefined> { ): Promise<void> {
const status = response.status const status = response.status
// We have hit ratelimit - this should not happen
if (status === HttpResponseCode.TooManyRequests) {
if (this.client !== undefined)
this.client.emit('rateLimit', {
method: data.method,
url: response.url,
body
})
reject(new Error('RateLimited'))
return
}
// It's a normal status code... just continue
if ( if (
(status >= 200 && status < 400) || (status >= 200 && status < 400) ||
status === HttpResponseCode.NoContent || status === HttpResponseCode.NoContent
status === HttpResponseCode.TooManyRequests
) )
return return
@ -322,9 +395,7 @@ export class RESTManager {
if (status === HttpResponseCode.Unauthorized) if (status === HttpResponseCode.Unauthorized)
reject( reject(
new DiscordAPIError( new DiscordAPIError(`Request was Unauthorized. Invalid Token.\n${text}`)
`Request was not successful (Unauthorized). Invalid Token.\n${text}`
)
) )
// At this point we know it is error // At this point we know it is error
@ -342,7 +413,7 @@ export class RESTManager {
} }
}) ?? {} }) ?? {}
).map((entry) => { ).map((entry) => {
return [entry[0], entry[1]._errors] return [entry[0], entry[1]._errors ?? []]
}) })
) )
} }
@ -379,7 +450,7 @@ export class RESTManager {
} }
/** /**
* Makes a Request to Discord API * Makes a Request to Discord API.
* @param method HTTP Method to use * @param method HTTP Method to use
* @param url URL of the Request * @param url URL of the Request
* @param body Body to send with Request * @param body Body to send with Request
@ -422,7 +493,18 @@ export class RESTManager {
let urlToUse = let urlToUse =
method === 'get' && query !== '' ? `${url}?${query}` : url method === 'get' && query !== '' ? `${url}?${query}` : url
if (this.client?.canary === true) { // It doesn't start with HTTP, that means it's an incomplete URL
if (!urlToUse.startsWith('http')) {
if (!urlToUse.startsWith('/')) urlToUse = `/${urlToUse}`
urlToUse =
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
baseEndpoints.DISCORD_API_URL +
'/v' +
baseEndpoints.DISCORD_API_VERSION +
urlToUse
}
if (this.canary === true && urlToUse.startsWith('http')) {
const split = urlToUse.split('//') const split = urlToUse.split('//')
urlToUse = split[0] + '//canary.' + split[1] urlToUse = split[0] + '//canary.' + split[1]
} }

View file

@ -47,7 +47,9 @@ export class Webhook {
rest: RESTManager rest: RESTManager
get url(): string { get url(): string {
return `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/webhooks/${this.id}/${this.token}` return `${DISCORD_API_URL}/v${
this.rest.version ?? DISCORD_API_VERSION
}/webhooks/${this.id}/${this.token}`
} }
constructor(data: WebhookPayload, client?: Client, rest?: RESTManager) { constructor(data: WebhookPayload, client?: Client, rest?: RESTManager) {
@ -170,7 +172,7 @@ export class Webhook {
* @param options Options to edit the Webhook. * @param options Options to edit the Webhook.
*/ */
async edit(options: WebhookEditOptions): Promise<Webhook> { async edit(options: WebhookEditOptions): Promise<Webhook> {
if (options.channelID !== undefined && this.rest.client === undefined) if (options.channelID !== undefined && this.client === undefined)
throw new Error('Authentication is required for editing Webhook Channel') throw new Error('Authentication is required for editing Webhook Channel')
if ( if (
options.avatar !== undefined && options.avatar !== undefined &&

View file

@ -11,6 +11,7 @@ import {
ChannelTypes, ChannelTypes,
GuildTextChannel GuildTextChannel
} from '../../mod.ts' } from '../../mod.ts'
import { Collector } from '../models/collectors.ts'
import { TOKEN } from './config.ts' import { TOKEN } from './config.ts'
const client = new Client({ const client = new Client({
@ -122,6 +123,26 @@ client.on('messageCreate', async (msg: Message) => {
) )
msg.channel.send(`Received: ${receivedMsg?.content}`) msg.channel.send(`Received: ${receivedMsg?.content}`)
} else if (msg.content.startsWith('!collect') === true) {
let count = parseInt(msg.content.replace(/\D/g, ''))
if (isNaN(count)) count = 5
await msg.channel.send(`Collecting ${count} messages for 5s`)
const coll = new Collector({
event: 'messageCreate',
filter: (m) => m.author.id === msg.author.id,
deinitOnEnd: true,
max: count,
timeout: 5000
})
coll.init(client)
coll.collect()
coll.on('start', () => msg.channel.send('[COL] Started'))
coll.on('end', () =>
msg.channel.send(`[COL] Ended. Collected Size: ${coll.collected.size}`)
)
coll.on('collect', (msg) =>
msg.channel.send(`[COL] Collect: ${msg.content}`)
)
} }
}) })

View file

@ -11,10 +11,7 @@ import {
GuildTextChannel GuildTextChannel
} from '../../mod.ts' } from '../../mod.ts'
import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts' import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts'
import { import { Manager, Player } from 'https://deno.land/x/lavadeno/mod.ts'
Manager,
Player
} from '../../deps.ts'
import { Interaction } from '../structures/slash.ts' import { Interaction } from '../structures/slash.ts'
import { slash } from '../models/client.ts' import { slash } from '../models/client.ts'
// import { SlashCommandOptionType } from '../types/slash.ts' // import { SlashCommandOptionType } from '../types/slash.ts'

View file

@ -9,6 +9,11 @@ export class MyClient extends Client {
console.log(`Logged in as ${this.user?.tag}!`) console.log(`Logged in as ${this.user?.tag}!`)
} }
@event('debug')
debugEvt(txt: string): void {
console.log(txt)
}
@slash() @slash()
send(d: Interaction): void { send(d: Interaction): void {
d.respond({ d.respond({
@ -92,5 +97,11 @@ export class MyClient extends Client {
} }
} }
const client = new MyClient() const client = new MyClient({
presence: {
status: 'dnd',
activity: { name: 'Slash Commands', type: 'LISTENING' }
}
})
client.connect(TOKEN, Intents.None) client.connect(TOKEN, Intents.None)