Merge pull request #148 from DjDeveloperr/vc-fix
refactor: voice join/leave
This commit is contained in:
		
						commit
						a25aef6400
					
				
					 14 changed files with 419 additions and 121 deletions
				
			
		|  | @ -1,4 +1,4 @@ | ||||||
| import { Interaction } from './src/structures/interactions.ts ' | import { Interaction } from './src/structures/interactions.ts' | ||||||
| import { | import { | ||||||
|   SlashCommandsManager, |   SlashCommandsManager, | ||||||
|   SlashClient, |   SlashClient, | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ import type { VoiceRegion } from '../types/voice.ts' | ||||||
| import { fetchAuto } from '../../deps.ts' | import { fetchAuto } from '../../deps.ts' | ||||||
| import type { DMChannel } from '../structures/dmChannel.ts' | import type { DMChannel } from '../structures/dmChannel.ts' | ||||||
| import { Template } from '../structures/template.ts' | import { Template } from '../structures/template.ts' | ||||||
|  | import { VoiceManager } from './voice.ts' | ||||||
| 
 | 
 | ||||||
| /** OS related properties sent with Gateway Identify */ | /** OS related properties sent with Gateway Identify */ | ||||||
| export interface ClientProperties { | export interface ClientProperties { | ||||||
|  | @ -38,7 +39,7 @@ export interface ClientOptions { | ||||||
|   /** Token of the Bot/User */ |   /** Token of the Bot/User */ | ||||||
|   token?: string |   token?: string | ||||||
|   /** Gateway Intents */ |   /** Gateway Intents */ | ||||||
|   intents?: GatewayIntents[] |   intents?: Array<GatewayIntents | keyof typeof GatewayIntents> | ||||||
|   /** Cache Adapter to use, defaults to Collections one */ |   /** Cache Adapter to use, defaults to Collections one */ | ||||||
|   cache?: ICacheAdapter |   cache?: ICacheAdapter | ||||||
|   /** Force New Session and don't use cached Session (by persistent caching) */ |   /** Force New Session and don't use cached Session (by persistent caching) */ | ||||||
|  | @ -77,10 +78,29 @@ export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||||
|   rest: RESTManager |   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 | ||||||
|  | 
 | ||||||
|  |   #token?: string | ||||||
|  | 
 | ||||||
|   /** Token of the Bot/User */ |   /** Token of the Bot/User */ | ||||||
|   token?: string |   get token(): string | undefined { | ||||||
|  |     return this.#token | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   set token(val: string | undefined) { | ||||||
|  |     this.#token = val | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** Cache Adapter */ |   /** Cache Adapter */ | ||||||
|   cache: ICacheAdapter = new DefaultCacheAdapter() |   get cache(): ICacheAdapter { | ||||||
|  |     return this.#cache | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   set cache(val: ICacheAdapter) { | ||||||
|  |     this.#cache = val | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   #cache: ICacheAdapter = new DefaultCacheAdapter() | ||||||
|  | 
 | ||||||
|   /** Gateway Intents */ |   /** Gateway Intents */ | ||||||
|   intents?: GatewayIntents[] |   intents?: GatewayIntents[] | ||||||
|   /** Whether to force new session or not */ |   /** Whether to force new session or not */ | ||||||
|  | @ -91,21 +111,26 @@ export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||||
|   reactionCacheLifetime: number = 3600000 |   reactionCacheLifetime: number = 3600000 | ||||||
|   /** Whether to fetch Uncached Message of Reaction or not? */ |   /** Whether to fetch Uncached Message of Reaction or not? */ | ||||||
|   fetchUncachedReactions: boolean = false |   fetchUncachedReactions: boolean = false | ||||||
|  | 
 | ||||||
|   /** Client Properties */ |   /** Client Properties */ | ||||||
|   clientProperties: ClientProperties |   readonly clientProperties!: ClientProperties | ||||||
|  | 
 | ||||||
|   /** Slash-Commands Management client */ |   /** Slash-Commands Management client */ | ||||||
|   slash: SlashClient |   slash: SlashClient | ||||||
|   /** Whether to fetch Gateway info or not */ |   /** Whether to fetch Gateway info or not */ | ||||||
|   fetchGatewayInfo: boolean = true |   fetchGatewayInfo: boolean = true | ||||||
| 
 | 
 | ||||||
|  |   /** Voice Connections Manager */ | ||||||
|  |   readonly voice = new VoiceManager(this) | ||||||
|  | 
 | ||||||
|   /** Users Manager, containing all Users cached */ |   /** Users Manager, containing all Users cached */ | ||||||
|   users: UsersManager = new UsersManager(this) |   readonly users: UsersManager = new UsersManager(this) | ||||||
|   /** Guilds Manager, providing cache & API interface to Guilds */ |   /** Guilds Manager, providing cache & API interface to Guilds */ | ||||||
|   guilds: GuildManager = new GuildManager(this) |   readonly guilds: GuildManager = new GuildManager(this) | ||||||
|   /** Channels Manager, providing cache interface to Channels */ |   /** Channels Manager, providing cache interface to Channels */ | ||||||
|   channels: ChannelsManager = new ChannelsManager(this) |   readonly channels: ChannelsManager = new ChannelsManager(this) | ||||||
|   /** Channels Manager, providing cache interface to Channels */ |   /** Channels Manager, providing cache interface to Channels */ | ||||||
|   emojis: EmojisManager = new EmojisManager(this) |   readonly emojis: EmojisManager = new EmojisManager(this) | ||||||
| 
 | 
 | ||||||
|   /** Last READY timestamp */ |   /** Last READY timestamp */ | ||||||
|   upSince?: Date |   upSince?: Date | ||||||
|  | @ -146,7 +171,9 @@ export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||||
|     super() |     super() | ||||||
|     this._id = options.id |     this._id = options.id | ||||||
|     this.token = options.token |     this.token = options.token | ||||||
|     this.intents = options.intents |     this.intents = options.intents?.map((e) => | ||||||
|  |       typeof e === 'string' ? GatewayIntents[e] : e | ||||||
|  |     ) | ||||||
|     this.shards = new ShardManager(this) |     this.shards = new ShardManager(this) | ||||||
|     this.forceNewSession = options.forceNewSession |     this.forceNewSession = options.forceNewSession | ||||||
|     if (options.cache !== undefined) this.cache = options.cache |     if (options.cache !== undefined) this.cache = options.cache | ||||||
|  | @ -172,14 +199,17 @@ export class Client extends HarmonyEventEmitter<ClientEvents> { | ||||||
|       ;(this as any)._decoratedEvents = undefined |       ;(this as any)._decoratedEvents = undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.clientProperties = |     Object.defineProperty(this, 'clientProperties', { | ||||||
|  |       value: | ||||||
|         options.clientProperties === undefined |         options.clientProperties === undefined | ||||||
|           ? { |           ? { | ||||||
|               os: Deno.build.os, |               os: Deno.build.os, | ||||||
|               browser: 'harmony', |               browser: 'harmony', | ||||||
|               device: 'harmony' |               device: 'harmony' | ||||||
|             } |             } | ||||||
|         : options.clientProperties |           : options.clientProperties, | ||||||
|  |       enumerable: false | ||||||
|  |     }) | ||||||
| 
 | 
 | ||||||
|     if (options.shard !== undefined) this.shard = options.shard |     if (options.shard !== undefined) this.shard = options.shard | ||||||
|     if (options.shardCount !== undefined) this.shardCount = options.shardCount |     if (options.shardCount !== undefined) this.shardCount = options.shardCount | ||||||
|  |  | ||||||
							
								
								
									
										116
									
								
								src/client/voice.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/client/voice.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,116 @@ | ||||||
|  | import type { VoiceServerUpdateData } from '../gateway/handlers/mod.ts' | ||||||
|  | import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||||
|  | import type { VoiceStateOptions } from '../gateway/mod.ts' | ||||||
|  | import { VoiceState } from '../structures/voiceState.ts' | ||||||
|  | import { ChannelTypes } from '../types/channel.ts' | ||||||
|  | import type { Guild } from '../structures/guild.ts' | ||||||
|  | import { HarmonyEventEmitter } from '../utils/events.ts' | ||||||
|  | import type { Client } from './client.ts' | ||||||
|  | 
 | ||||||
|  | export interface VoiceServerData extends VoiceServerUpdateData { | ||||||
|  |   userID: string | ||||||
|  |   sessionID: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface VoiceChannelJoinOptions extends VoiceStateOptions { | ||||||
|  |   timeout?: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class VoiceManager extends HarmonyEventEmitter<{ | ||||||
|  |   voiceStateUpdate: [VoiceState] | ||||||
|  | }> { | ||||||
|  |   #pending = new Map<string, [number, CallableFunction]>() | ||||||
|  | 
 | ||||||
|  |   readonly client!: Client | ||||||
|  | 
 | ||||||
|  |   constructor(client: Client) { | ||||||
|  |     super() | ||||||
|  |     Object.defineProperty(this, 'client', { | ||||||
|  |       value: client, | ||||||
|  |       enumerable: false | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async join( | ||||||
|  |     channel: string | VoiceChannel, | ||||||
|  |     options?: VoiceChannelJoinOptions | ||||||
|  |   ): Promise<VoiceServerData> { | ||||||
|  |     const id = typeof channel === 'string' ? channel : channel.id | ||||||
|  |     const chan = await this.client.channels.get<VoiceChannel>(id) | ||||||
|  |     if (chan === undefined) throw new Error('Voice Channel not cached') | ||||||
|  |     if ( | ||||||
|  |       chan.type !== ChannelTypes.GUILD_VOICE && | ||||||
|  |       chan.type !== ChannelTypes.GUILD_STAGE_VOICE | ||||||
|  |     ) | ||||||
|  |       throw new Error('Cannot join non-voice channel') | ||||||
|  | 
 | ||||||
|  |     const pending = this.#pending.get(chan.guild.id) | ||||||
|  |     if (pending !== undefined) { | ||||||
|  |       clearTimeout(pending[0]) | ||||||
|  |       pending[1](new Error('Voice Connection timed out')) | ||||||
|  |       this.#pending.delete(chan.guild.id) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return await new Promise((resolve, reject) => { | ||||||
|  |       let vcdata: VoiceServerData | ||||||
|  |       let done = 0 | ||||||
|  | 
 | ||||||
|  |       const onVoiceStateAdd = (state: VoiceState): void => { | ||||||
|  |         if (state.user.id !== this.client.user?.id) return | ||||||
|  |         if (state.channel?.id !== id) return | ||||||
|  |         this.client.off('voiceStateAdd', onVoiceStateAdd) | ||||||
|  |         done++ | ||||||
|  |         vcdata = vcdata ?? {} | ||||||
|  |         vcdata.sessionID = state.sessionID | ||||||
|  |         vcdata.userID = state.user.id | ||||||
|  |         if (done >= 2) { | ||||||
|  |           this.#pending.delete(chan.guild.id) | ||||||
|  |           resolve(vcdata) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => { | ||||||
|  |         if (data.guild.id !== chan.guild.id) return | ||||||
|  |         vcdata = Object.assign(vcdata ?? {}, data) | ||||||
|  |         this.client.off('voiceServerUpdate', onVoiceServerUpdate) | ||||||
|  |         done++ | ||||||
|  |         if (done >= 2) { | ||||||
|  |           this.#pending.delete(chan.guild.id) | ||||||
|  |           resolve(vcdata) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.client.shards | ||||||
|  |         .get(chan.guild.shardID)! | ||||||
|  |         .updateVoiceState(chan.guild.id, chan.id, options) | ||||||
|  | 
 | ||||||
|  |       this.on('voiceStateUpdate', onVoiceStateAdd) | ||||||
|  |       this.client.on('voiceServerUpdate', onVoiceServerUpdate) | ||||||
|  | 
 | ||||||
|  |       const timer = setTimeout(() => { | ||||||
|  |         if (done < 2) { | ||||||
|  |           this.client.off('voiceServerUpdate', onVoiceServerUpdate) | ||||||
|  |           this.client.off('voiceStateAdd', onVoiceStateAdd) | ||||||
|  |           reject( | ||||||
|  |             new Error( | ||||||
|  |               "Connection timed out - couldn't connect to Voice Channel" | ||||||
|  |             ) | ||||||
|  |           ) | ||||||
|  |         } | ||||||
|  |       }, options?.timeout ?? 1000 * 30) | ||||||
|  | 
 | ||||||
|  |       this.#pending.set(chan.guild.id, [timer, reject]) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async leave(guildOrID: Guild | string): Promise<void> { | ||||||
|  |     const id = typeof guildOrID === 'string' ? guildOrID : guildOrID.id | ||||||
|  |     const guild = await this.client.guilds.get(id) | ||||||
|  |     if (guild === undefined) throw new Error('Guild not cached') | ||||||
|  |     // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
 | ||||||
|  |     const vcs = await guild.voiceStates.get(this.client.user?.id!) | ||||||
|  |     if (vcs === undefined) throw new Error('Not in Voice Channel') | ||||||
|  | 
 | ||||||
|  |     this.client.shards.get(guild.shardID)!.updateVoiceState(guild, undefined) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -33,17 +33,15 @@ export const voiceStateUpdate: GatewayEventHandler = async ( | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   await guild.voiceStates.set(d.user_id, d) |   await guild.voiceStates.set(d.user_id, d) | ||||||
|   const newVoiceState = await guild.voiceStates.get(d.user_id) |   const newVoiceState = (await guild.voiceStates.get(d.user_id))! | ||||||
|  | 
 | ||||||
|  |   if (d.user_id === gateway.client.user!.id) { | ||||||
|  |     gateway.client.voice.emit('voiceStateUpdate', newVoiceState) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   if (voiceState === undefined) { |   if (voiceState === undefined) { | ||||||
|     gateway.client.emit( |     gateway.client.emit('voiceStateAdd', newVoiceState) | ||||||
|       'voiceStateAdd', |  | ||||||
|       (newVoiceState as unknown) as VoiceState |  | ||||||
|     ) |  | ||||||
|   } else { |   } else { | ||||||
|     gateway.client.emit( |     gateway.client.emit('voiceStateUpdate', voiceState, newVoiceState) | ||||||
|       'voiceStateUpdate', |  | ||||||
|       voiceState, |  | ||||||
|       (newVoiceState as unknown) as VoiceState |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -62,7 +62,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|   lastPingTimestamp = 0 |   lastPingTimestamp = 0 | ||||||
|   sessionID?: string |   sessionID?: string | ||||||
|   private heartbeatServerResponded = false |   private heartbeatServerResponded = false | ||||||
|   client: Client |   client!: Client | ||||||
|   cache: GatewayCache |   cache: GatewayCache | ||||||
|   private timedIdentify: number | null = null |   private timedIdentify: number | null = null | ||||||
|   shards?: number[] |   shards?: number[] | ||||||
|  | @ -70,7 +70,7 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
| 
 | 
 | ||||||
|   constructor(client: Client, shards?: number[]) { |   constructor(client: Client, shards?: number[]) { | ||||||
|     super() |     super() | ||||||
|     this.client = client |     Object.defineProperty(this, 'client', { value: client, enumerable: false }) | ||||||
|     this.cache = new GatewayCache(client) |     this.cache = new GatewayCache(client) | ||||||
|     this.shards = shards |     this.shards = shards | ||||||
|   } |   } | ||||||
|  | @ -371,13 +371,13 @@ export class Gateway extends HarmonyEventEmitter<GatewayTypedEvents> { | ||||||
|             : channel?.id, |             : channel?.id, | ||||||
|         self_mute: |         self_mute: | ||||||
|           channel === undefined |           channel === undefined | ||||||
|             ? undefined |             ? false | ||||||
|             : voiceOptions.mute === undefined |             : voiceOptions.mute === undefined | ||||||
|             ? false |             ? false | ||||||
|             : voiceOptions.mute, |             : voiceOptions.mute, | ||||||
|         self_deaf: |         self_deaf: | ||||||
|           channel === undefined |           channel === undefined | ||||||
|             ? undefined |             ? false | ||||||
|             : voiceOptions.deaf === undefined |             : voiceOptions.deaf === undefined | ||||||
|             ? false |             ? false | ||||||
|             : voiceOptions.deaf |             : voiceOptions.deaf | ||||||
|  |  | ||||||
|  | @ -50,11 +50,21 @@ export type SlashClientEvents = { | ||||||
| export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> { | export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> { | ||||||
|   id: string | (() => string) |   id: string | (() => string) | ||||||
|   client?: Client |   client?: Client | ||||||
|   token?: string | 
 | ||||||
|  |   #token?: string | ||||||
|  | 
 | ||||||
|  |   get token(): string | undefined { | ||||||
|  |     return this.#token | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   set token(val: string | undefined) { | ||||||
|  |     this.#token = val | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   enabled: boolean = true |   enabled: boolean = true | ||||||
|   commands: SlashCommandsManager |   commands: SlashCommandsManager | ||||||
|   handlers: SlashCommandHandler[] = [] |   handlers: SlashCommandHandler[] = [] | ||||||
|   rest: RESTManager |   readonly rest!: RESTManager | ||||||
|   modules: SlashModule[] = [] |   modules: SlashModule[] = [] | ||||||
|   publicKey?: string |   publicKey?: string | ||||||
| 
 | 
 | ||||||
|  | @ -65,7 +75,14 @@ export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> { | ||||||
|     if (id === undefined) |     if (id === undefined) | ||||||
|       throw new Error('ID could not be found. Pass at least client or token') |       throw new Error('ID could not be found. Pass at least client or token') | ||||||
|     this.id = id |     this.id = id | ||||||
|     this.client = options.client | 
 | ||||||
|  |     if (options.client !== undefined) { | ||||||
|  |       Object.defineProperty(this, 'client', { | ||||||
|  |         value: options.client, | ||||||
|  |         enumerable: false | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     this.token = options.token |     this.token = options.token | ||||||
|     this.publicKey = options.publicKey |     this.publicKey = options.publicKey | ||||||
| 
 | 
 | ||||||
|  | @ -88,14 +105,17 @@ export class SlashClient extends HarmonyEventEmitter<SlashClientEvents> { | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     this.rest = |     Object.defineProperty(this, 'rest', { | ||||||
|  |       value: | ||||||
|         options.client === undefined |         options.client === undefined | ||||||
|           ? options.rest === undefined |           ? options.rest === undefined | ||||||
|             ? new RESTManager({ |             ? new RESTManager({ | ||||||
|                 token: this.token |                 token: this.token | ||||||
|               }) |               }) | ||||||
|             : options.rest |             : options.rest | ||||||
|         : options.client.rest |           : options.client.rest, | ||||||
|  |       enumerable: false | ||||||
|  |     }) | ||||||
| 
 | 
 | ||||||
|     this.client?.on( |     this.client?.on( | ||||||
|       'interactionCreate', |       'interactionCreate', | ||||||
|  |  | ||||||
|  | @ -198,12 +198,15 @@ export class SlashBuilder { | ||||||
| 
 | 
 | ||||||
| /** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */ | /** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */ | ||||||
| export class SlashCommandsManager { | export class SlashCommandsManager { | ||||||
|   slash: SlashClient |   readonly slash!: SlashClient | ||||||
|   rest: RESTManager |   readonly rest!: RESTManager | ||||||
| 
 | 
 | ||||||
|   constructor(client: SlashClient) { |   constructor(client: SlashClient) { | ||||||
|     this.slash = client |     Object.defineProperty(this, 'slash', { value: client, enumerable: false }) | ||||||
|     this.rest = client.rest |     Object.defineProperty(this, 'rest', { | ||||||
|  |       enumerable: false, | ||||||
|  |       value: client.rest | ||||||
|  |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Get all Global Slash Commands */ |   /** Get all Global Slash Commands */ | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import type { Client } from '../client/mod.ts' | import type { Client } from '../client/mod.ts' | ||||||
|  | import { Base } from '../structures/base.ts' | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | @ -6,15 +7,14 @@ import { Collection } from '../utils/collection.ts' | ||||||
|  * |  * | ||||||
|  * You should not be making Managers yourself. |  * You should not be making Managers yourself. | ||||||
|  */ |  */ | ||||||
| export class BaseManager<T, T2> { | export class BaseManager<T, T2> extends Base { | ||||||
|   client: Client |  | ||||||
|   /** Caches Name or Key used to differentiate caches */ |   /** Caches Name or Key used to differentiate caches */ | ||||||
|   cacheName: string |   cacheName: string | ||||||
|   /** Which data type does this cache have */ |   /** Which data type does this cache have */ | ||||||
|   DataType: any |   DataType: any | ||||||
| 
 | 
 | ||||||
|   constructor(client: Client, cacheName: string, DataType: any) { |   constructor(client: Client, cacheName: string, DataType: any) { | ||||||
|     this.client = client |     super(client) | ||||||
|     this.cacheName = cacheName |     this.cacheName = cacheName | ||||||
|     this.DataType = DataType |     this.DataType = DataType | ||||||
|   } |   } | ||||||
|  | @ -87,4 +87,8 @@ export class BaseManager<T, T2> { | ||||||
|   flush(): any { |   flush(): any { | ||||||
|     return this.client.cache.deleteCache(this.cacheName) |     return this.client.cache.deleteCache(this.cacheName) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   [Deno.customInspect](): string { | ||||||
|  |     return `Manager(${this.cacheName})` | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,15 +1,15 @@ | ||||||
| import type { Client } from '../client/mod.ts' | import type { Client } from '../client/mod.ts' | ||||||
|  | import { Base } from '../structures/base.ts' | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
| /** Child Managers validate data from their parents i.e. from Managers */ | /** Child Managers validate data from their parents i.e. from Managers */ | ||||||
| export class BaseChildManager<T, T2> { | export class BaseChildManager<T, T2> extends Base { | ||||||
|   client: Client |  | ||||||
|   /** Parent Manager */ |   /** Parent Manager */ | ||||||
|   parent: BaseManager<T, T2> |   parent: BaseManager<T, T2> | ||||||
| 
 | 
 | ||||||
|   constructor(client: Client, parent: BaseManager<T, T2>) { |   constructor(client: Client, parent: BaseManager<T, T2>) { | ||||||
|     this.client = client |     super(client) | ||||||
|     this.parent = parent |     this.parent = parent | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -62,4 +62,8 @@ export class BaseChildManager<T, T2> { | ||||||
|       if (fetchValue !== undefined) return fetchValue |       if (fetchValue !== undefined) return fetchValue | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   [Deno.customInspect](): string { | ||||||
|  |     return `ChildManager(${this.parent.cacheName})` | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -103,8 +103,18 @@ export class RESTManager { | ||||||
|    *   ``` |    *   ``` | ||||||
|    */ |    */ | ||||||
|   api: APIMap |   api: APIMap | ||||||
|  | 
 | ||||||
|  |   #token?: string | (() => string | undefined) | ||||||
|  | 
 | ||||||
|   /** Token being used for Authorization */ |   /** Token being used for Authorization */ | ||||||
|   token?: string | (() => string | undefined) |   get token(): string | (() => string | undefined) | undefined { | ||||||
|  |     return this.#token | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   set token(val: string | (() => string | undefined) | undefined) { | ||||||
|  |     this.#token = val | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** Token Type of the Token if any */ |   /** Token Type of the Token if any */ | ||||||
|   tokenType: TokenType = TokenType.Bot |   tokenType: TokenType = TokenType.Bot | ||||||
|   /** Headers object which patch the current ones */ |   /** Headers object which patch the current ones */ | ||||||
|  | @ -114,13 +124,13 @@ export class RESTManager { | ||||||
|   /** Whether REST Manager is using Canary API */ |   /** Whether REST Manager is using Canary API */ | ||||||
|   canary?: boolean |   canary?: boolean | ||||||
|   /** Optional Harmony Client object */ |   /** Optional Harmony Client object */ | ||||||
|   client?: Client |   readonly client?: Client | ||||||
|   endpoints: RESTEndpoints |   endpoints: RESTEndpoints | ||||||
|   requestTimeout = 30000 |   requestTimeout = 30000 | ||||||
|   timers: Set<number> = new Set() |   readonly timers!: Set<number> | ||||||
|   apiURL = Constants.DISCORD_API_URL |   apiURL = Constants.DISCORD_API_URL | ||||||
| 
 | 
 | ||||||
|   handlers = new Collection<string, BucketHandler>() |   readonly handlers!: Collection<string, BucketHandler> | ||||||
|   globalLimit = Infinity |   globalLimit = Infinity | ||||||
|   globalRemaining = this.globalLimit |   globalRemaining = this.globalLimit | ||||||
|   globalReset: number | null = null |   globalReset: number | null = null | ||||||
|  | @ -136,11 +146,28 @@ export class RESTManager { | ||||||
|     if (options?.tokenType !== undefined) this.tokenType = options.tokenType |     if (options?.tokenType !== undefined) this.tokenType = options.tokenType | ||||||
|     if (options?.userAgent !== undefined) this.userAgent = options.userAgent |     if (options?.userAgent !== undefined) this.userAgent = options.userAgent | ||||||
|     if (options?.canary !== undefined) this.canary = options.canary |     if (options?.canary !== undefined) this.canary = options.canary | ||||||
|     if (options?.client !== undefined) this.client = options.client |  | ||||||
|     if (options?.retryLimit !== undefined) this.retryLimit = options.retryLimit |     if (options?.retryLimit !== undefined) this.retryLimit = options.retryLimit | ||||||
|     if (options?.requestTimeout !== undefined) |     if (options?.requestTimeout !== undefined) | ||||||
|       this.requestTimeout = options.requestTimeout |       this.requestTimeout = options.requestTimeout | ||||||
|  | 
 | ||||||
|  |     if (options?.client !== undefined) { | ||||||
|  |       Object.defineProperty(this, 'client', { | ||||||
|  |         value: options.client, | ||||||
|  |         enumerable: false | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     this.endpoints = new RESTEndpoints(this) |     this.endpoints = new RESTEndpoints(this) | ||||||
|  | 
 | ||||||
|  |     Object.defineProperty(this, 'timers', { | ||||||
|  |       value: new Set(), | ||||||
|  |       enumerable: false | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     Object.defineProperty(this, 'handlers', { | ||||||
|  |       value: new Collection<string, BucketHandler>(), | ||||||
|  |       enumerable: false | ||||||
|  |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   setTimeout(fn: (...args: any[]) => any, ms: number): number { |   setTimeout(fn: (...args: any[]) => any, ms: number): number { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,3 @@ | ||||||
| import type { VoiceServerUpdateData } from '../gateway/handlers/mod.ts' |  | ||||||
| import type { VoiceStateOptions } from '../gateway/mod.ts' |  | ||||||
| import type { Client } from '../client/mod.ts' | import type { Client } from '../client/mod.ts' | ||||||
| import type { | import type { | ||||||
|   GuildVoiceChannelPayload, |   GuildVoiceChannelPayload, | ||||||
|  | @ -9,14 +7,13 @@ import type { | ||||||
| import { CHANNEL } from '../types/endpoint.ts' | import { CHANNEL } from '../types/endpoint.ts' | ||||||
| import { GuildChannel } from './channel.ts' | import { GuildChannel } from './channel.ts' | ||||||
| import type { Guild } from './guild.ts' | import type { Guild } from './guild.ts' | ||||||
| import type { VoiceState } from './voiceState.ts' |  | ||||||
| import { GuildChannelVoiceStatesManager } from '../managers/guildChannelVoiceStates.ts' | import { GuildChannelVoiceStatesManager } from '../managers/guildChannelVoiceStates.ts' | ||||||
| import type { User } from './user.ts' | import type { User } from './user.ts' | ||||||
| import type { Member } from './member.ts' | import type { Member } from './member.ts' | ||||||
| 
 | import type { | ||||||
| export interface VoiceServerData extends VoiceServerUpdateData { |   VoiceChannelJoinOptions, | ||||||
|   sessionID: string |   VoiceServerData | ||||||
| } | } from '../client/voice.ts' | ||||||
| 
 | 
 | ||||||
| export class VoiceChannel extends GuildChannel { | export class VoiceChannel extends GuildChannel { | ||||||
|   bitrate: string |   bitrate: string | ||||||
|  | @ -34,65 +31,13 @@ export class VoiceChannel extends GuildChannel { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Join the Voice Channel */ |   /** Join the Voice Channel */ | ||||||
|   async join( |   async join(options?: VoiceChannelJoinOptions): Promise<VoiceServerData> { | ||||||
|     options?: VoiceStateOptions & { onlyJoin?: boolean } |     return this.client.voice.join(this.id, options) | ||||||
|   ): Promise<VoiceServerData> { |  | ||||||
|     return await new Promise((resolve, reject) => { |  | ||||||
|       let vcdata: VoiceServerData |  | ||||||
|       let sessionID: string |  | ||||||
|       let done = 0 |  | ||||||
| 
 |  | ||||||
|       const onVoiceStateAdd = (state: VoiceState): void => { |  | ||||||
|         if (state.user.id !== this.client.user?.id) return |  | ||||||
|         if (state.channel?.id !== this.id) return |  | ||||||
|         this.client.off('voiceStateAdd', onVoiceStateAdd) |  | ||||||
|         done++ |  | ||||||
|         sessionID = state.sessionID |  | ||||||
|         if (done >= 2) { |  | ||||||
|           vcdata.sessionID = sessionID |  | ||||||
|           if (options?.onlyJoin !== true) { |  | ||||||
|           } |  | ||||||
|           resolve(vcdata) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       const onVoiceServerUpdate = (data: VoiceServerUpdateData): void => { |  | ||||||
|         if (data.guild.id !== this.guild.id) return |  | ||||||
|         vcdata = (data as unknown) as VoiceServerData |  | ||||||
|         this.client.off('voiceServerUpdate', onVoiceServerUpdate) |  | ||||||
|         done++ |  | ||||||
|         if (done >= 2) { |  | ||||||
|           vcdata.sessionID = sessionID |  | ||||||
|           resolve(vcdata) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       this.client.shards |  | ||||||
|         .get(this.guild.shardID) |  | ||||||
|         ?.updateVoiceState(this.guild.id, this.id, options) |  | ||||||
| 
 |  | ||||||
|       this.client.on('voiceStateAdd', onVoiceStateAdd) |  | ||||||
|       this.client.on('voiceServerUpdate', onVoiceServerUpdate) |  | ||||||
| 
 |  | ||||||
|       setTimeout(() => { |  | ||||||
|         if (done < 2) { |  | ||||||
|           this.client.off('voiceServerUpdate', onVoiceServerUpdate) |  | ||||||
|           this.client.off('voiceStateAdd', onVoiceStateAdd) |  | ||||||
|           reject( |  | ||||||
|             new Error( |  | ||||||
|               "Connection timed out - couldn't connect to Voice Channel" |  | ||||||
|             ) |  | ||||||
|           ) |  | ||||||
|         } |  | ||||||
|       }, 1000 * 60) |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Leave the Voice Channel */ |   /** Leave the Voice Channel */ | ||||||
|   leave(): void { |   async leave(): Promise<void> { | ||||||
|     this.client.shards |     return this.client.voice.leave(this.guild) | ||||||
|       .get(this.guild.shardID) |  | ||||||
|       ?.updateVoiceState(this.guild.id, undefined) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   readFromData(data: GuildVoiceChannelPayload): void { |   readFromData(data: GuildVoiceChannelPayload): void { | ||||||
|  | @ -116,6 +61,14 @@ export class VoiceChannel extends GuildChannel { | ||||||
|     return new VoiceChannel(this.client, resp, this.guild) |     return new VoiceChannel(this.client, resp, this.guild) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async setBitrate(rate: number | undefined): Promise<VoiceChannel> { | ||||||
|  |     return await this.edit({ bitrate: rate }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async setUserLimit(limit: number | undefined): Promise<VoiceChannel> { | ||||||
|  |     return await this.edit({ userLimit: limit }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async disconnectMember( |   async disconnectMember( | ||||||
|     member: User | Member | string |     member: User | Member | string | ||||||
|   ): Promise<Member | undefined> { |   ): Promise<Member | undefined> { | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								test/eval.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								test/eval.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | /* eslint-disable no-eval */ | ||||||
|  | import * as discord from '../mod.ts' | ||||||
|  | import { TOKEN } from './config.ts' | ||||||
|  | 
 | ||||||
|  | const client = new discord.Client({ | ||||||
|  |   token: TOKEN, | ||||||
|  |   intents: ['GUILD_MESSAGES', 'GUILDS'] | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | client.on('messageCreate', async (msg) => { | ||||||
|  |   if (msg.author.id !== '422957901716652033') return | ||||||
|  | 
 | ||||||
|  |   if (msg.content.startsWith('.eval') === true) { | ||||||
|  |     let code = msg.content.slice(5).trim() | ||||||
|  |     if (code.startsWith('```') === true) code = code.slice(3).trim() | ||||||
|  |     if (code.endsWith('```') === true) code = code.substr(0, code.length - 3) | ||||||
|  |     try { | ||||||
|  |       const result = await eval(code) | ||||||
|  |       msg.reply( | ||||||
|  |         `\`\`\`js\n${Deno.inspect(result).substr(0, 2000 - 20)}\n\`\`\`` | ||||||
|  |       ) | ||||||
|  |     } catch (e) { | ||||||
|  |       msg.reply(`\`\`\`js\n${e}\n\`\`\``, { | ||||||
|  |         allowedMentions: { replied_user: false } | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | client.connect().then(() => console.log('Ready!')) | ||||||
							
								
								
									
										87
									
								
								test/trigger.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								test/trigger.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | ||||||
|  | /* eslint-disable @typescript-eslint/strict-boolean-expressions */ | ||||||
|  | /* eslint-disable no-control-regex */ | ||||||
|  | import { Client, Embed } from '../mod.ts' | ||||||
|  | import { TOKEN } from './config.ts' | ||||||
|  | 
 | ||||||
|  | const client = new Client({ | ||||||
|  |   token: TOKEN, | ||||||
|  |   intents: ['GUILDS', 'GUILD_MESSAGES'] | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const NAME_MATCH = /[^a-zA-Z0-9_]/ | ||||||
|  | const STD_REGEX = /\/?std(@[\x00-\x2e\x30-\xff]+)?\/([a-zA-Z0-9]+)(\/[\S\s]+)?/ | ||||||
|  | const X_REGEX = /\/?x\/([a-zA-Z0-9]+)(@[\x00-\x2e\x30-\xff]+)?(\/[\S\s]+)?/ | ||||||
|  | 
 | ||||||
|  | export async function fetchModule(name: string): Promise<any> { | ||||||
|  |   if (name.match(NAME_MATCH) !== null) return null | ||||||
|  |   return fetch(`https://api.deno.land/modules/${name}`, { | ||||||
|  |     credentials: 'omit', | ||||||
|  |     headers: { | ||||||
|  |       'User-Agent': | ||||||
|  |         'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0', | ||||||
|  |       Accept: 'application/json', | ||||||
|  |       'Accept-Language': 'en-US,en;q=0.5', | ||||||
|  |       Pragma: 'no-cache', | ||||||
|  |       'Cache-Control': 'no-cache' | ||||||
|  |     }, | ||||||
|  |     referrer: 'https://deno.land/x', | ||||||
|  |     mode: 'cors' | ||||||
|  |   }) | ||||||
|  |     .then((res) => { | ||||||
|  |       if (res.status !== 200) throw new Error('not found') | ||||||
|  |       return res | ||||||
|  |     }) | ||||||
|  |     .then((r) => r.json()) | ||||||
|  |     .then((json) => { | ||||||
|  |       if (!json.success) throw new Error('failed') | ||||||
|  |       return json | ||||||
|  |     }) | ||||||
|  |     .then((data) => data.data) | ||||||
|  |     .catch(() => null) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | client.on('messageCreate', async (msg) => { | ||||||
|  |   if (msg.author.bot === true) return | ||||||
|  | 
 | ||||||
|  |   let match | ||||||
|  |   if ( | ||||||
|  |     (match = msg.content.match(STD_REGEX)) || | ||||||
|  |     (match = msg.content.match(X_REGEX)) | ||||||
|  |   ) { | ||||||
|  |     let x = match[0].trim() | ||||||
|  | 
 | ||||||
|  |     if (!x.startsWith('/')) x = `/${x}` | ||||||
|  | 
 | ||||||
|  |     let type | ||||||
|  |     if (x.startsWith('/std')) { | ||||||
|  |       x = x.slice(4) | ||||||
|  |       type = 'std' | ||||||
|  |     } else { | ||||||
|  |       x = x.slice(3) | ||||||
|  |       type = 'x' | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     x = x.trim() | ||||||
|  |     const name = x.split('/')[0].split('@')[0] | ||||||
|  |     const mod = await fetchModule(type === 'std' ? 'std' : name) | ||||||
|  |     if (mod === null) return | ||||||
|  | 
 | ||||||
|  |     msg.channel.send( | ||||||
|  |       new Embed() | ||||||
|  |         .setColor('#7289DA') | ||||||
|  |         .setURL( | ||||||
|  |           `https://deno.land/${type}${ | ||||||
|  |             x.startsWith('/') || x.startsWith('@') ? '' : '/' | ||||||
|  |           }${x}` | ||||||
|  |         ) | ||||||
|  |         .setTitle( | ||||||
|  |           `${type}${x.startsWith('/') || x.startsWith('@') ? '' : '/'}${x}` | ||||||
|  |         ) | ||||||
|  |         .setDescription(mod.description ?? 'No description.') | ||||||
|  |         .setFooter(`${mod.star_count ?? 0}`, 'https://kokoro.pw/colleague.png') | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | console.log('Connecting...') | ||||||
|  | client.connect().then(() => console.log('Ready!')) | ||||||
							
								
								
									
										26
									
								
								test/vc.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								test/vc.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | import * as discord from '../mod.ts' | ||||||
|  | import { TOKEN } from './config.ts' | ||||||
|  | 
 | ||||||
|  | const client = new discord.Client({ | ||||||
|  |   token: TOKEN, | ||||||
|  |   intents: ['GUILDS', 'GUILD_VOICE_STATES', 'GUILD_MESSAGES'] | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | client.on('messageCreate', async (msg) => { | ||||||
|  |   if (msg.author.bot === true || msg.guild === undefined) return | ||||||
|  | 
 | ||||||
|  |   if (msg.content === '!join') { | ||||||
|  |     const vs = await msg.guild.voiceStates.get(msg.author.id) | ||||||
|  |     if (vs === undefined) return msg.reply("You're not in Voice Channel!") | ||||||
|  |     const data = await vs.channel!.join() | ||||||
|  |     console.log(data) | ||||||
|  |     msg.reply('Joined voice channel.') | ||||||
|  |   } else if (msg.content === '!leave') { | ||||||
|  |     const vs = await msg.guild.voiceStates.get(msg.client.user!.id!) | ||||||
|  |     if (vs === undefined) return msg.reply("I'm not in Voice Channel!") | ||||||
|  |     await vs.channel!.leave() | ||||||
|  |     msg.reply('Left voice channel.') | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | client.connect().then(() => console.log('Ready!')) | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue