Merge pull request #81 from DjDeveloperr/slash
Quick fix for Events to use Manager#_delete and adding full Invite support
This commit is contained in:
		
						commit
						68fa36ce3c
					
				
					 25 changed files with 289 additions and 74 deletions
				
			
		|  | @ -7,7 +7,7 @@ export const channelDelete: GatewayEventHandler = async ( | ||||||
| ) => { | ) => { | ||||||
|   const channel = await gateway.client.channels.get(d.id) |   const channel = await gateway.client.channels.get(d.id) | ||||||
|   if (channel !== undefined) { |   if (channel !== undefined) { | ||||||
|     await gateway.client.channels.delete(d.id) |     await gateway.client.channels._delete(d.id) | ||||||
|     gateway.client.emit('channelDelete', channel) |     gateway.client.emit('channelDelete', channel) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -30,5 +30,5 @@ export const guildCreate: GatewayEventHandler = async ( | ||||||
|   if (hasGuild === undefined) { |   if (hasGuild === undefined) { | ||||||
|     // It wasn't lazy load, so emit event
 |     // It wasn't lazy load, so emit event
 | ||||||
|     gateway.client.emit('guildCreate', guild) |     gateway.client.emit('guildCreate', guild) | ||||||
|   } |   } else gateway.client.emit('guildLoaded', guild) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ export const guildDelete: GatewayEventHandler = async ( | ||||||
|     await guild.channels.flush() |     await guild.channels.flush() | ||||||
|     await guild.roles.flush() |     await guild.roles.flush() | ||||||
|     await guild.presences.flush() |     await guild.presences.flush() | ||||||
|     await gateway.client.guilds.delete(d.id) |     await gateway.client.guilds._delete(d.id) | ||||||
| 
 | 
 | ||||||
|     gateway.client.emit('guildDelete', guild) |     gateway.client.emit('guildDelete', guild) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ export const guildRoleDelete: GatewayEventHandler = async ( | ||||||
|   const role = await guild.roles.get(d.role_id) |   const role = await guild.roles.get(d.role_id) | ||||||
|   // Shouldn't happen either
 |   // Shouldn't happen either
 | ||||||
|   if (role === undefined) return |   if (role === undefined) return | ||||||
|  |   await guild.roles._delete(d.role_id) | ||||||
| 
 | 
 | ||||||
|   gateway.client.emit('guildRoleDelete', role) |   gateway.client.emit('guildRoleDelete', role) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -154,6 +154,11 @@ export interface ClientEvents { | ||||||
|    * @param guild The new Guild object |    * @param guild The new Guild object | ||||||
|    */ |    */ | ||||||
|   guildCreate: [guild: Guild] |   guildCreate: [guild: Guild] | ||||||
|  |   /** | ||||||
|  |    * A Guild was successfully loaded. | ||||||
|  |    * @param guild The Guild object | ||||||
|  |    */ | ||||||
|  |   guildLoaded: [guild: Guild] | ||||||
|   /** |   /** | ||||||
|    * A Guild in which Client was either deleted, or bot was kicked |    * A Guild in which Client was either deleted, or bot was kicked | ||||||
|    * @param guild The Guild object |    * @param guild The Guild object | ||||||
|  |  | ||||||
|  | @ -19,7 +19,6 @@ export const inviteDelete: GatewayEventHandler = async ( | ||||||
|   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 |   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||||
|   const cachedGuild = await gateway.client.guilds.get(d.guild_id!) |   const cachedGuild = await gateway.client.guilds.get(d.guild_id!) | ||||||
| 
 | 
 | ||||||
|   // TODO(DjDeveloperr): Make it support self-bots and make Guild not always defined
 |  | ||||||
|   if (cachedInvite === undefined) { |   if (cachedInvite === undefined) { | ||||||
|     const uncachedInvite: PartialInvitePayload = { |     const uncachedInvite: PartialInvitePayload = { | ||||||
|       guild: (cachedGuild as unknown) as Guild, |       guild: (cachedGuild as unknown) as Guild, | ||||||
|  | @ -28,7 +27,7 @@ export const inviteDelete: GatewayEventHandler = async ( | ||||||
|     } |     } | ||||||
|     return gateway.client.emit('inviteDeleteUncached', uncachedInvite) |     return gateway.client.emit('inviteDeleteUncached', uncachedInvite) | ||||||
|   } else { |   } else { | ||||||
|     await guild.invites.delete(d.code) |     await guild.invites._delete(d.code) | ||||||
|     gateway.client.emit('inviteDelete', cachedInvite) |     gateway.client.emit('inviteDelete', cachedInvite) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -15,6 +15,6 @@ export const messageDelete: GatewayEventHandler = async ( | ||||||
|   const message = await channel.messages.get(d.id) |   const message = await channel.messages.get(d.id) | ||||||
|   if (message === undefined) |   if (message === undefined) | ||||||
|     return gateway.client.emit('messageDeleteUncached', d) |     return gateway.client.emit('messageDeleteUncached', d) | ||||||
|   await channel.messages.delete(d.id) |   await channel.messages._delete(d.id) | ||||||
|   gateway.client.emit('messageDelete', message) |   gateway.client.emit('messageDelete', message) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -188,7 +188,9 @@ export class Gateway extends EventEmitter { | ||||||
|         this.reconnect() |         this.reconnect() | ||||||
|         break |         break | ||||||
|       case GatewayCloseCodes.UNKNOWN_OPCODE: |       case GatewayCloseCodes.UNKNOWN_OPCODE: | ||||||
|         throw new Error("Unknown OP Code was sent. This shouldn't happen!") |         throw new Error( | ||||||
|  |           "Invalid OP Code or Payload was sent. This shouldn't happen!" | ||||||
|  |         ) | ||||||
|       case GatewayCloseCodes.DECODE_ERROR: |       case GatewayCloseCodes.DECODE_ERROR: | ||||||
|         throw new Error("Invalid Payload was sent. This shouldn't happen!") |         throw new Error("Invalid Payload was sent. This shouldn't happen!") | ||||||
|       case GatewayCloseCodes.NOT_AUTHENTICATED: |       case GatewayCloseCodes.NOT_AUTHENTICATED: | ||||||
|  | @ -320,8 +322,8 @@ export class Gateway extends EventEmitter { | ||||||
|       op: GatewayOpcodes.REQUEST_GUILD_MEMBERS, |       op: GatewayOpcodes.REQUEST_GUILD_MEMBERS, | ||||||
|       d: { |       d: { | ||||||
|         guild_id: guild, |         guild_id: guild, | ||||||
|         query: options.query, |         query: options.query ?? '', | ||||||
|         limit: options.limit, |         limit: options.limit ?? 0, | ||||||
|         presences: options.presences, |         presences: options.presences, | ||||||
|         user_ids: options.users, |         user_ids: options.users, | ||||||
|         nonce |         nonce | ||||||
|  | @ -387,14 +389,13 @@ export class Gateway extends EventEmitter { | ||||||
| 
 | 
 | ||||||
|   send(data: GatewayResponse): boolean { |   send(data: GatewayResponse): boolean { | ||||||
|     if (this.websocket.readyState !== this.websocket.OPEN) return false |     if (this.websocket.readyState !== this.websocket.OPEN) return false | ||||||
|     this.websocket.send( |     const packet = JSON.stringify({ | ||||||
|       JSON.stringify({ |       op: data.op, | ||||||
|         op: data.op, |       d: data.d, | ||||||
|         d: data.d, |       s: typeof data.s === 'number' ? data.s : null, | ||||||
|         s: typeof data.s === 'number' ? data.s : null, |       t: data.t === undefined ? null : data.t | ||||||
|         t: data.t === undefined ? null : data.t |     }) | ||||||
|       }) |     this.websocket.send(packet) | ||||||
|     ) |  | ||||||
|     return true |     return true | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -29,6 +29,7 @@ export class ChannelsManager extends BaseManager<ChannelPayload, Channel> { | ||||||
|     const arr = await (this.client.cache.array( |     const arr = await (this.client.cache.array( | ||||||
|       this.cacheName |       this.cacheName | ||||||
|     ) as ChannelPayload[]) |     ) as ChannelPayload[]) | ||||||
|  |     if (arr === undefined) return [] | ||||||
|     const result: any[] = [] |     const result: any[] = [] | ||||||
|     for (const elem of arr) { |     for (const elem of arr) { | ||||||
|       let guild |       let guild | ||||||
|  |  | ||||||
|  | @ -5,11 +5,14 @@ import { CategoryChannel } from '../structures/guildCategoryChannel.ts' | ||||||
| import { GuildTextChannel } from '../structures/textChannel.ts' | import { GuildTextChannel } from '../structures/textChannel.ts' | ||||||
| import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | import { VoiceChannel } from '../structures/guildVoiceChannel.ts' | ||||||
| import { | import { | ||||||
|  |   ChannelTypes, | ||||||
|   GuildCategoryChannelPayload, |   GuildCategoryChannelPayload, | ||||||
|  |   GuildChannelPayload, | ||||||
|   GuildTextChannelPayload, |   GuildTextChannelPayload, | ||||||
|   GuildVoiceChannelPayload |   GuildVoiceChannelPayload, | ||||||
|  |   Overwrite | ||||||
| } from '../types/channel.ts' | } from '../types/channel.ts' | ||||||
| import { CHANNEL } from '../types/endpoint.ts' | import { CHANNEL, GUILD_CHANNELS } from '../types/endpoint.ts' | ||||||
| import { BaseChildManager } from './baseChild.ts' | import { BaseChildManager } from './baseChild.ts' | ||||||
| import { ChannelsManager } from './channels.ts' | import { ChannelsManager } from './channels.ts' | ||||||
| 
 | 
 | ||||||
|  | @ -19,6 +22,19 @@ export type GuildChannelPayloads = | ||||||
|   | GuildCategoryChannelPayload |   | GuildCategoryChannelPayload | ||||||
| export type GuildChannel = GuildTextChannel | VoiceChannel | CategoryChannel | export type GuildChannel = GuildTextChannel | VoiceChannel | CategoryChannel | ||||||
| 
 | 
 | ||||||
|  | export interface CreateChannelOptions { | ||||||
|  |   name: string | ||||||
|  |   type?: ChannelTypes | ||||||
|  |   topic?: string | ||||||
|  |   bitrate?: number | ||||||
|  |   userLimit?: number | ||||||
|  |   rateLimitPerUser?: number | ||||||
|  |   position?: number | ||||||
|  |   permissionOverwrites?: Overwrite[] | ||||||
|  |   parent?: CategoryChannel | string | ||||||
|  |   nsfw?: boolean | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export class GuildChannelsManager extends BaseChildManager< | export class GuildChannelsManager extends BaseChildManager< | ||||||
|   GuildChannelPayloads, |   GuildChannelPayloads, | ||||||
|   GuildChannel |   GuildChannel | ||||||
|  | @ -55,4 +71,32 @@ export class GuildChannelsManager extends BaseChildManager< | ||||||
|     } |     } | ||||||
|     return true |     return true | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** Create a new Guild Channel */ | ||||||
|  |   async create(options: CreateChannelOptions): Promise<GuildChannel> { | ||||||
|  |     if (options.name === undefined) | ||||||
|  |       throw new Error('name is required for GuildChannelsManager#create') | ||||||
|  |     const res = ((await this.client.rest.post(GUILD_CHANNELS(this.guild.id)), | ||||||
|  |     { | ||||||
|  |       name: options.name, | ||||||
|  |       type: options.type, | ||||||
|  |       topic: options.topic, | ||||||
|  |       bitrate: options.bitrate, | ||||||
|  |       user_limit: options.userLimit, | ||||||
|  |       rate_limit_per_user: options.rateLimitPerUser, | ||||||
|  |       position: options.position, | ||||||
|  |       permission_overwrites: options.permissionOverwrites, | ||||||
|  |       parent_id: | ||||||
|  |         options.parent === undefined | ||||||
|  |           ? undefined | ||||||
|  |           : typeof options.parent === 'object' | ||||||
|  |           ? options.parent.id | ||||||
|  |           : options.parent, | ||||||
|  |       nsfw: options.nsfw | ||||||
|  |     }) as unknown) as GuildChannelPayload | ||||||
|  | 
 | ||||||
|  |     await this.set(res.id, res) | ||||||
|  |     const channel = await this.get(res.id) | ||||||
|  |     return (channel as unknown) as GuildChannel | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -17,6 +17,13 @@ export class GuildVoiceStatesManager extends BaseManager< | ||||||
|     this.guild = guild |     this.guild = guild | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** Get Client's Voice State in the Guild */ | ||||||
|  |   async me(): Promise<VoiceState | undefined> { | ||||||
|  |     const member = await this.guild.me() | ||||||
|  |     return await this.get(member.id) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Get a Voice State by User ID */ | ||||||
|   async get(key: string): Promise<VoiceState | undefined> { |   async get(key: string): Promise<VoiceState | undefined> { | ||||||
|     const raw = await this._get(key) |     const raw = await this._get(key) | ||||||
|     if (raw === undefined) return |     if (raw === undefined) return | ||||||
|  |  | ||||||
|  | @ -1,10 +1,24 @@ | ||||||
|  | import { GuildTextChannel, User } from '../../mod.ts' | ||||||
| import { Client } from '../models/client.ts' | import { Client } from '../models/client.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import { Guild } from '../structures/guild.ts' | ||||||
| import { Invite } from '../structures/invite.ts' | import { Invite } from '../structures/invite.ts' | ||||||
| import { INVITE } from '../types/endpoint.ts' | import { CHANNEL_INVITES, GUILD_INVITES, INVITE } from '../types/endpoint.ts' | ||||||
| import { InvitePayload } from '../types/invite.ts' | import { InvitePayload } from '../types/invite.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
|  | export enum InviteTargetUserType { | ||||||
|  |   STREAM = 1 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface CreateInviteOptions { | ||||||
|  |   maxAge?: number | ||||||
|  |   maxUses?: number | ||||||
|  |   temporary?: boolean | ||||||
|  |   unique?: boolean | ||||||
|  |   targetUser?: string | User | ||||||
|  |   targetUserType?: InviteTargetUserType | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export class InviteManager extends BaseManager<InvitePayload, Invite> { | export class InviteManager extends BaseManager<InvitePayload, Invite> { | ||||||
|   guild: Guild |   guild: Guild | ||||||
| 
 | 
 | ||||||
|  | @ -20,10 +34,10 @@ export class InviteManager extends BaseManager<InvitePayload, Invite> { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Fetch an Invite */ |   /** Fetch an Invite */ | ||||||
|   async fetch(id: string): Promise<Invite> { |   async fetch(id: string, withCounts: boolean = true): Promise<Invite> { | ||||||
|     return await new Promise((resolve, reject) => { |     return await new Promise((resolve, reject) => { | ||||||
|       this.client.rest |       this.client.rest | ||||||
|         .get(INVITE(id)) |         .get(`${INVITE(id)}${withCounts ? '?with_counts=true' : ''}`) | ||||||
|         .then(async (data) => { |         .then(async (data) => { | ||||||
|           this.set(id, data as InvitePayload) |           this.set(id, data as InvitePayload) | ||||||
|           const newInvite = await this.get(data.code) |           const newInvite = await this.get(data.code) | ||||||
|  | @ -33,6 +47,57 @@ export class InviteManager extends BaseManager<InvitePayload, Invite> { | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** Fetch all Invites of a Guild or a specific Channel */ | ||||||
|  |   async fetchAll(channel?: string | GuildTextChannel): Promise<Invite[]> { | ||||||
|  |     const rawInvites = (await this.client.rest.get( | ||||||
|  |       channel === undefined | ||||||
|  |         ? GUILD_INVITES(this.guild.id) | ||||||
|  |         : CHANNEL_INVITES(typeof channel === 'string' ? channel : channel.id) | ||||||
|  |     )) as InvitePayload[] | ||||||
|  | 
 | ||||||
|  |     const res: Invite[] = [] | ||||||
|  | 
 | ||||||
|  |     for (const raw of rawInvites) { | ||||||
|  |       await this.set(raw.code, raw) | ||||||
|  |       res.push(new Invite(this.client, raw)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return res | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Delete an Invite */ | ||||||
|  |   async delete(invite: string | Invite): Promise<boolean> { | ||||||
|  |     await this.client.rest.delete( | ||||||
|  |       INVITE(typeof invite === 'string' ? invite : invite.code) | ||||||
|  |     ) | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Create an Invite */ | ||||||
|  |   async create( | ||||||
|  |     channel: string | GuildTextChannel, | ||||||
|  |     options?: CreateInviteOptions | ||||||
|  |   ): Promise<Invite> { | ||||||
|  |     const raw = ((await this.client.rest.post( | ||||||
|  |       CHANNEL_INVITES(typeof channel === 'string' ? channel : channel.id), | ||||||
|  |       { | ||||||
|  |         max_age: options?.maxAge, | ||||||
|  |         max_uses: options?.maxUses, | ||||||
|  |         temporary: options?.temporary, | ||||||
|  |         unique: options?.unique, | ||||||
|  |         target_user: | ||||||
|  |           options?.targetUser === undefined | ||||||
|  |             ? undefined | ||||||
|  |             : typeof options.targetUser === 'string' | ||||||
|  |             ? options.targetUser | ||||||
|  |             : options.targetUser.id, | ||||||
|  |         target_user_type: options?.targetUserType | ||||||
|  |       } | ||||||
|  |     )) as unknown) as InvitePayload | ||||||
|  | 
 | ||||||
|  |     return new Invite(this.client, raw) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async fromPayload(invites: InvitePayload[]): Promise<boolean> { |   async fromPayload(invites: InvitePayload[]): Promise<boolean> { | ||||||
|     for (const invite of invites) { |     for (const invite of invites) { | ||||||
|       await this.set(invite.code, invite) |       await this.set(invite.code, invite) | ||||||
|  |  | ||||||
|  | @ -3,8 +3,13 @@ import { Emoji } from '../structures/emoji.ts' | ||||||
| import { Guild } from '../structures/guild.ts' | import { Guild } from '../structures/guild.ts' | ||||||
| import { Message } from '../structures/message.ts' | import { Message } from '../structures/message.ts' | ||||||
| import { MessageReaction } from '../structures/messageReaction.ts' | import { MessageReaction } from '../structures/messageReaction.ts' | ||||||
|  | import { User } from '../structures/user.ts' | ||||||
| import { Reaction } from '../types/channel.ts' | import { Reaction } from '../types/channel.ts' | ||||||
| import { MESSAGE_REACTION, MESSAGE_REACTIONS } from '../types/endpoint.ts' | import { | ||||||
|  |   MESSAGE_REACTION, | ||||||
|  |   MESSAGE_REACTIONS, | ||||||
|  |   MESSAGE_REACTION_USER | ||||||
|  | } from '../types/endpoint.ts' | ||||||
| import { BaseManager } from './base.ts' | import { BaseManager } from './base.ts' | ||||||
| 
 | 
 | ||||||
| export class MessageReactionsManager extends BaseManager< | export class MessageReactionsManager extends BaseManager< | ||||||
|  | @ -77,4 +82,23 @@ export class MessageReactionsManager extends BaseManager< | ||||||
|     ) |     ) | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** Remove a specific Emoji from Reactions */ | ||||||
|  |   async removeUser( | ||||||
|  |     emoji: Emoji | string, | ||||||
|  |     user: User | string | ||||||
|  |   ): Promise<MessageReactionsManager> { | ||||||
|  |     const val = encodeURIComponent( | ||||||
|  |       (typeof emoji === 'object' ? emoji.id ?? emoji.name : emoji) as string | ||||||
|  |     ) | ||||||
|  |     await this.client.rest.delete( | ||||||
|  |       MESSAGE_REACTION_USER( | ||||||
|  |         this.message.channel.id, | ||||||
|  |         this.message.id, | ||||||
|  |         val, | ||||||
|  |         typeof user === 'string' ? user : user.id | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |     return this | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| import { Client } from '../models/client.ts' | import { Client } from '../models/client.ts' | ||||||
| import { MessageReaction } from '../structures/messageReaction.ts' | import { MessageReaction } from '../structures/messageReaction.ts' | ||||||
|  | import { User } from '../structures/user.ts' | ||||||
| import { UsersManager } from './users.ts' | import { UsersManager } from './users.ts' | ||||||
| 
 | 
 | ||||||
| export class ReactionUsersManager extends UsersManager { | export class ReactionUsersManager extends UsersManager { | ||||||
|  | @ -10,4 +11,14 @@ export class ReactionUsersManager extends UsersManager { | ||||||
|     this.cacheName = `reaction_users:${reaction.message.id}` |     this.cacheName = `reaction_users:${reaction.message.id}` | ||||||
|     this.reaction = reaction |     this.reaction = reaction | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** Remove all Users from this Reaction */ | ||||||
|  |   async removeAll(): Promise<void> { | ||||||
|  |     await this.reaction.message.reactions.removeEmoji(this.reaction.emoji) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Remove a specific User from this Reaction */ | ||||||
|  |   async remove(user: User | string): Promise<void> { | ||||||
|  |     await this.reaction.message.reactions.removeUser(this.reaction.emoji, user) | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,9 +1,5 @@ | ||||||
| import { Collection } from '../utils/collection.ts' | import { Collection } from '../utils/collection.ts' | ||||||
| import { | import { connect, Redis, RedisConnectOptions } from '../../deps.ts' | ||||||
|   connect, |  | ||||||
|   Redis, |  | ||||||
|   RedisConnectOptions |  | ||||||
| } from '../../deps.ts' |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. |  * ICacheAdapter is the interface to be implemented by Cache Adapters for them to be usable with Harmony. | ||||||
|  |  | ||||||
|  | @ -134,7 +134,6 @@ export class Client extends EventEmitter { | ||||||
|     handler: (interaction: Interaction) => any |     handler: (interaction: Interaction) => any | ||||||
|   }> |   }> | ||||||
| 
 | 
 | ||||||
|   _decoratedSlashModules?: SlashModule[] |  | ||||||
|   _id?: string |   _id?: string | ||||||
| 
 | 
 | ||||||
|   /** Shard on which this Client is */ |   /** Shard on which this Client is */ | ||||||
|  | @ -266,6 +265,7 @@ export class Client extends EventEmitter { | ||||||
|     this.gateway = new Gateway(this, token, intents) |     this.gateway = new Gateway(this, token, intents) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** 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, | ||||||
|     checkFunction: (...args: ClientEvents[K]) => boolean, |     checkFunction: (...args: ClientEvents[K]) => boolean, | ||||||
|  | @ -291,6 +291,7 @@ export class Client extends EventEmitter { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** 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) { | ||||||
|     const listener = ((client as unknown) as { |     const listener = ((client as unknown) as { | ||||||
|  | @ -307,11 +308,11 @@ export function event(name?: keyof ClientEvents) { | ||||||
| 
 | 
 | ||||||
| /** Decorator to create a Slash Command handler */ | /** Decorator to create a Slash Command handler */ | ||||||
| export function slash(name?: string, guild?: string) { | export function slash(name?: string, guild?: string) { | ||||||
|   return function (client: Client | SlashModule, prop: string) { |   return function (client: Client | SlashClient | SlashModule, prop: string) { | ||||||
|     if (client._decoratedSlash === undefined) client._decoratedSlash = [] |     if (client._decoratedSlash === undefined) client._decoratedSlash = [] | ||||||
|     const item = (client as { [name: string]: any })[prop] |     const item = (client as { [name: string]: any })[prop] | ||||||
|     if (typeof item !== 'function') { |     if (typeof item !== 'function') { | ||||||
|       client._decoratedSlash.push(item) |       throw new Error('@slash decorator requires a function') | ||||||
|     } else |     } else | ||||||
|       client._decoratedSlash.push({ |       client._decoratedSlash.push({ | ||||||
|         name: name ?? prop, |         name: name ?? prop, | ||||||
|  | @ -323,12 +324,11 @@ export function slash(name?: string, guild?: string) { | ||||||
| 
 | 
 | ||||||
| /** Decorator to create a Sub-Slash Command handler */ | /** Decorator to create a Sub-Slash Command handler */ | ||||||
| export function subslash(parent: string, name?: string, guild?: string) { | export function subslash(parent: string, name?: string, guild?: string) { | ||||||
|   return function (client: Client | SlashModule, prop: string) { |   return function (client: Client | SlashModule | SlashClient, prop: string) { | ||||||
|     if (client._decoratedSlash === undefined) client._decoratedSlash = [] |     if (client._decoratedSlash === undefined) client._decoratedSlash = [] | ||||||
|     const item = (client as { [name: string]: any })[prop] |     const item = (client as { [name: string]: any })[prop] | ||||||
|     if (typeof item !== 'function') { |     if (typeof item !== 'function') { | ||||||
|       item.parent = parent |       throw new Error('@subslash decorator requires a function') | ||||||
|       client._decoratedSlash.push(item) |  | ||||||
|     } else |     } else | ||||||
|       client._decoratedSlash.push({ |       client._decoratedSlash.push({ | ||||||
|         parent, |         parent, | ||||||
|  | @ -350,9 +350,7 @@ export function groupslash( | ||||||
|     if (client._decoratedSlash === undefined) client._decoratedSlash = [] |     if (client._decoratedSlash === undefined) client._decoratedSlash = [] | ||||||
|     const item = (client as { [name: string]: any })[prop] |     const item = (client as { [name: string]: any })[prop] | ||||||
|     if (typeof item !== 'function') { |     if (typeof item !== 'function') { | ||||||
|       item.parent = parent |       throw new Error('@groupslash decorator requires a function') | ||||||
|       item.group = group |  | ||||||
|       client._decoratedSlash.push(item) |  | ||||||
|     } else |     } else | ||||||
|       client._decoratedSlash.push({ |       client._decoratedSlash.push({ | ||||||
|         group, |         group, | ||||||
|  | @ -363,14 +361,3 @@ export function groupslash( | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| /** Decorator to add a Slash Module to Client */ |  | ||||||
| export function slashModule() { |  | ||||||
|   return function (client: Client, prop: string) { |  | ||||||
|     if (client._decoratedSlashModules === undefined) |  | ||||||
|       client._decoratedSlashModules = [] |  | ||||||
| 
 |  | ||||||
|     const mod = ((client as unknown) as { [key: string]: any })[prop] |  | ||||||
|     client._decoratedSlashModules.push(mod) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -382,6 +382,7 @@ export class CommandClient extends Client implements CommandClientOptions { | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** Command decorator */ | ||||||
| export function command(options?: CommandOptions) { | export function command(options?: CommandOptions) { | ||||||
|   return function (target: CommandClient | Extension, name: string) { |   return function (target: CommandClient | Extension, name: string) { | ||||||
|     if (target._decoratedCommands === undefined) target._decoratedCommands = {} |     if (target._decoratedCommands === undefined) target._decoratedCommands = {} | ||||||
|  | @ -390,10 +391,8 @@ export function command(options?: CommandOptions) { | ||||||
|       [name: string]: (ctx: CommandContext) => any |       [name: string]: (ctx: CommandContext) => any | ||||||
|     })[name] |     })[name] | ||||||
| 
 | 
 | ||||||
|     if (prop instanceof Command) { |     if (typeof prop !== 'function') | ||||||
|       target._decoratedCommands[prop.name] = prop |       throw new Error('@command decorator can only be used on functions') | ||||||
|       return |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     const command = new Command() |     const command = new Command() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,11 +14,11 @@ import { RESTManager } from './rest.ts' | ||||||
| import { SlashModule } from './slashModule.ts' | import { SlashModule } from './slashModule.ts' | ||||||
| import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts' | import { verify as edverify } from 'https://deno.land/x/ed25519/mod.ts' | ||||||
| import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts' | import { Buffer } from 'https://deno.land/std@0.80.0/node/buffer.ts' | ||||||
| import { | import type { | ||||||
|   Request as ORequest, |   Request as ORequest, | ||||||
|   Response as OResponse |   Response as OResponse | ||||||
| } from 'https://deno.land/x/opine@1.0.0/src/types.ts' | } from 'https://deno.land/x/opine@1.0.0/src/types.ts' | ||||||
| import { Context } from 'https://deno.land/x/oak@v6.4.0/mod.ts' | import type { Context } from 'https://deno.land/x/oak@v6.4.0/mod.ts' | ||||||
| 
 | 
 | ||||||
| export class SlashCommand { | export class SlashCommand { | ||||||
|   slash: SlashCommandsManager |   slash: SlashCommandsManager | ||||||
|  | @ -353,8 +353,6 @@ export class SlashClient { | ||||||
|     handler: (interaction: Interaction) => any |     handler: (interaction: Interaction) => any | ||||||
|   }> |   }> | ||||||
| 
 | 
 | ||||||
|   _decoratedSlashModules?: SlashModule[] |  | ||||||
| 
 |  | ||||||
|   constructor(options: SlashOptions) { |   constructor(options: SlashOptions) { | ||||||
|     let id = options.id |     let id = options.id | ||||||
|     if (options.token !== undefined) id = atob(options.token?.split('.')[0]) |     if (options.token !== undefined) id = atob(options.token?.split('.')[0]) | ||||||
|  | @ -376,24 +374,12 @@ export class SlashClient { | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (this.client?._decoratedSlashModules !== undefined) { |  | ||||||
|       this.client._decoratedSlashModules.forEach((e) => { |  | ||||||
|         this.modules.push(e) |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (this._decoratedSlash !== undefined) { |     if (this._decoratedSlash !== undefined) { | ||||||
|       this._decoratedSlash.forEach((e) => { |       this._decoratedSlash.forEach((e) => { | ||||||
|         this.handlers.push(e) |         this.handlers.push(e) | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (this._decoratedSlashModules !== undefined) { |  | ||||||
|       this._decoratedSlashModules.forEach((e) => { |  | ||||||
|         this.modules.push(e) |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this.rest = |     this.rest = | ||||||
|       options.client === undefined |       options.client === undefined | ||||||
|         ? options.rest === undefined |         ? options.rest === undefined | ||||||
|  | @ -418,11 +404,13 @@ export class SlashClient { | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** Load a Slash Module */ | ||||||
|   loadModule(module: SlashModule): SlashClient { |   loadModule(module: SlashModule): SlashClient { | ||||||
|     this.modules.push(module) |     this.modules.push(module) | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** Get all Handlers. Including Slash Modules */ | ||||||
|   getHandlers(): SlashCommandHandler[] { |   getHandlers(): SlashCommandHandler[] { | ||||||
|     let res = this.handlers |     let res = this.handlers | ||||||
|     for (const mod of this.modules) { |     for (const mod of this.modules) { | ||||||
|  | @ -438,6 +426,7 @@ export class SlashClient { | ||||||
|     return res |     return res | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** Get Handler for an Interaction. Supports nested sub commands and sub command groups. */ | ||||||
|   private _getCommand(i: Interaction): SlashCommandHandler | undefined { |   private _getCommand(i: Interaction): SlashCommandHandler | undefined { | ||||||
|     return this.getHandlers().find((e) => { |     return this.getHandlers().find((e) => { | ||||||
|       const hasGroupOrParent = e.group !== undefined || e.parent !== undefined |       const hasGroupOrParent = e.group !== undefined || e.parent !== undefined | ||||||
|  | @ -467,6 +456,10 @@ export class SlashClient { | ||||||
|     if (interaction.type !== InteractionType.APPLICATION_COMMAND) return |     if (interaction.type !== InteractionType.APPLICATION_COMMAND) return | ||||||
| 
 | 
 | ||||||
|     const cmd = this._getCommand(interaction) |     const cmd = this._getCommand(interaction) | ||||||
|  |     if (cmd?.group !== undefined) | ||||||
|  |       interaction.data.options = interaction.data.options[0].options ?? [] | ||||||
|  |     if (cmd?.parent !== undefined) | ||||||
|  |       interaction.data.options = interaction.data.options[0].options ?? [] | ||||||
| 
 | 
 | ||||||
|     if (cmd === undefined) return |     if (cmd === undefined) return | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,8 +13,6 @@ export class GroupDMChannel extends Channel { | ||||||
|     this.name = data.name |     this.name = data.name | ||||||
|     this.icon = data.icon |     this.icon = data.icon | ||||||
|     this.ownerID = data.owner_id |     this.ownerID = data.owner_id | ||||||
|     // TODO: Cache in Gateway Event Code
 |  | ||||||
|     // cache.set('groupchannel', this.id, this)
 |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   readFromData(data: GroupDMChannelPayload): void { |   readFromData(data: GroupDMChannelPayload): void { | ||||||
|  |  | ||||||
|  | @ -8,9 +8,13 @@ import { | ||||||
|   IntegrationExpireBehavior |   IntegrationExpireBehavior | ||||||
| } from '../types/guild.ts' | } from '../types/guild.ts' | ||||||
| import { Base } from './base.ts' | import { Base } from './base.ts' | ||||||
| import { RolesManager } from '../managers/roles.ts' | import { CreateGuildRoleOptions, RolesManager } from '../managers/roles.ts' | ||||||
| import { InviteManager } from '../managers/invites.ts' | import { InviteManager } from '../managers/invites.ts' | ||||||
| import { GuildChannelsManager } from '../managers/guildChannels.ts' | import { | ||||||
|  |   CreateChannelOptions, | ||||||
|  |   GuildChannel, | ||||||
|  |   GuildChannelsManager | ||||||
|  | } from '../managers/guildChannels.ts' | ||||||
| import { MembersManager } from '../managers/members.ts' | import { MembersManager } from '../managers/members.ts' | ||||||
| import { Role } from './role.ts' | import { Role } from './role.ts' | ||||||
| import { GuildEmojisManager } from '../managers/guildEmojis.ts' | import { GuildEmojisManager } from '../managers/guildEmojis.ts' | ||||||
|  | @ -297,6 +301,16 @@ export class Guild extends Base { | ||||||
|     return raw.map((e) => new GuildIntegration(this.client, e)) |     return raw.map((e) => new GuildIntegration(this.client, e)) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** Create a new Guild Channel */ | ||||||
|  |   async createChannel(options: CreateChannelOptions): Promise<GuildChannel> { | ||||||
|  |     return this.channels.create(options) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** Create a new Guild Role */ | ||||||
|  |   async createRole(options?: CreateGuildRoleOptions): Promise<Role> { | ||||||
|  |     return this.roles.create(options) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Chunks the Guild Members, i.e. cache them. |    * Chunks the Guild Members, i.e. cache them. | ||||||
|    * @param options Options regarding the Members Request |    * @param options Options regarding the Members Request | ||||||
|  | @ -327,7 +341,6 @@ export class Guild extends Base { | ||||||
|           } |           } | ||||||
|         }, timeout) |         }, timeout) | ||||||
|       } |       } | ||||||
|       resolve(this) |  | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -16,10 +16,32 @@ export class Invite extends Base { | ||||||
|   approximatePresenceCount?: number |   approximatePresenceCount?: number | ||||||
|   approximateMemberCount?: number |   approximateMemberCount?: number | ||||||
| 
 | 
 | ||||||
|  |   /** Number of times Invite was used. This is an Invite Metadata property (not always available) */ | ||||||
|  |   uses?: number | ||||||
|  |   /** Max number of times this Invite can be used. This is an Invite Metadata property (not always available) */ | ||||||
|  |   maxUses?: number | ||||||
|  |   /** Max age of the Invite in seconds. This is an Invite Metadata property (not always available) */ | ||||||
|  |   maxAge?: number | ||||||
|  |   /** Whether Invite is temporary or not. This is an Invite Metadata property (not always available) */ | ||||||
|  |   temporary?: boolean | ||||||
|  |   /** Timestamp (string) when Invite was created. This is an Invite Metadata property (not always available) */ | ||||||
|  |   createdAtTimestamp?: string | ||||||
|  | 
 | ||||||
|  |   /** Timestamp (Date) when Invite was created. This is an Invite Metadata property (not always available) */ | ||||||
|  |   get createdAt(): Date | undefined { | ||||||
|  |     return this.createdAtTimestamp === undefined | ||||||
|  |       ? undefined | ||||||
|  |       : new Date(this.createdAtTimestamp) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   get link(): string { |   get link(): string { | ||||||
|     return `https://discord.gg/${this.code}` |     return `https://discord.gg/${this.code}` | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   toString(): string { | ||||||
|  |     return this.link | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   constructor(client: Client, data: InvitePayload) { |   constructor(client: Client, data: InvitePayload) { | ||||||
|     super(client) |     super(client) | ||||||
|     this.code = data.code |     this.code = data.code | ||||||
|  | @ -30,6 +52,12 @@ export class Invite extends Base { | ||||||
|     this.targetUserType = data.target_user_type |     this.targetUserType = data.target_user_type | ||||||
|     this.approximateMemberCount = data.approximate_member_count |     this.approximateMemberCount = data.approximate_member_count | ||||||
|     this.approximatePresenceCount = data.approximate_presence_count |     this.approximatePresenceCount = data.approximate_presence_count | ||||||
|  | 
 | ||||||
|  |     this.uses = (data as any).uses | ||||||
|  |     this.maxUses = (data as any).maxUses | ||||||
|  |     this.maxAge = (data as any).maxAge | ||||||
|  |     this.temporary = (data as any).temporary | ||||||
|  |     this.createdAtTimestamp = (data as any).createdAtTimestamp | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** Delete an invite. Requires the MANAGE_CHANNELS permission on the channel this invite belongs to, or MANAGE_GUILD to remove any invite across the guild. Returns an invite object on success. Fires a Invite Delete Gateway event. */ |   /** Delete an invite. Requires the MANAGE_CHANNELS permission on the channel this invite belongs to, or MANAGE_GUILD to remove any invite across the guild. Returns an invite object on success. Fires a Invite Delete Gateway event. */ | ||||||
|  | @ -49,5 +77,12 @@ export class Invite extends Base { | ||||||
|       data.approximate_member_count ?? this.approximateMemberCount |       data.approximate_member_count ?? this.approximateMemberCount | ||||||
|     this.approximatePresenceCount = |     this.approximatePresenceCount = | ||||||
|       data.approximate_presence_count ?? this.approximatePresenceCount |       data.approximate_presence_count ?? this.approximatePresenceCount | ||||||
|  | 
 | ||||||
|  |     this.uses = (data as any).uses ?? this.uses | ||||||
|  |     this.maxUses = (data as any).maxUses ?? this.maxUses | ||||||
|  |     this.maxAge = (data as any).maxAge ?? this.maxAge | ||||||
|  |     this.temporary = (data as any).temporary ?? this.temporary | ||||||
|  |     this.createdAtTimestamp = | ||||||
|  |       (data as any).createdAtTimestamp ?? this.createdAtTimestamp | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | import { CreateInviteOptions } from '../managers/invites.ts' | ||||||
| import { MessagesManager } from '../managers/messages.ts' | import { MessagesManager } from '../managers/messages.ts' | ||||||
| import { Client } from '../models/client.ts' | import { Client } from '../models/client.ts' | ||||||
| import { | import { | ||||||
|  | @ -22,6 +23,7 @@ import { Channel } from './channel.ts' | ||||||
| import { Embed } from './embed.ts' | import { Embed } from './embed.ts' | ||||||
| import { Emoji } from './emoji.ts' | import { Emoji } from './emoji.ts' | ||||||
| import { Guild } from './guild.ts' | import { Guild } from './guild.ts' | ||||||
|  | import { Invite } from './invite.ts' | ||||||
| import { Member } from './member.ts' | import { Member } from './member.ts' | ||||||
| import { Message } from './message.ts' | import { Message } from './message.ts' | ||||||
| import { User } from './user.ts' | import { User } from './user.ts' | ||||||
|  | @ -319,4 +321,9 @@ export class GuildTextChannel extends TextChannel { | ||||||
| 
 | 
 | ||||||
|     return this |     return this | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** Create an Invite for this Channel */ | ||||||
|  |   async createInvite(options?: CreateInviteOptions): Promise<Invite> { | ||||||
|  |     return this.guild.invites.create(this.id, options) | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										27
									
								
								src/test/chunk.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/test/chunk.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | import { Client, Intents } from '../../mod.ts' | ||||||
|  | import { TOKEN } from './config.ts' | ||||||
|  | 
 | ||||||
|  | const client = new Client() | ||||||
|  | 
 | ||||||
|  | client.on('debug', console.log) | ||||||
|  | 
 | ||||||
|  | client.on('ready', () => { | ||||||
|  |   console.log(`Logged in as ${client.user?.tag}!`) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | client.on('guildLoaded', async (guild) => { | ||||||
|  |   if (guild.id !== '783319033205751809') return | ||||||
|  |   const arr = await guild.channels.array() | ||||||
|  |   console.log(arr.length) | ||||||
|  |   guild | ||||||
|  |     .chunk({ presences: true }, true) | ||||||
|  |     .then((guild) => { | ||||||
|  |       console.log(`Chunked guild:`, guild.id) | ||||||
|  |     }) | ||||||
|  |     .catch((e) => { | ||||||
|  |       console.log(`Failed to Chunk: ${guild.id} - ${e}`) | ||||||
|  |     }) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | console.log('Connecting...') | ||||||
|  | client.connect(TOKEN, Intents.All) | ||||||
|  | @ -4,6 +4,8 @@ import { TOKEN } from './config.ts' | ||||||
| 
 | 
 | ||||||
| export const slash = new SlashClient({ token: TOKEN }) | export const slash = new SlashClient({ token: TOKEN }) | ||||||
| 
 | 
 | ||||||
|  | console.log(slash.modules) | ||||||
|  | 
 | ||||||
| // Cmd objects come here
 | // Cmd objects come here
 | ||||||
| const commands: SlashCommandPartial[] = [] | const commands: SlashCommandPartial[] = [] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ const GUILD_BANS = (guildID: string): string => | ||||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/bans` |   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/bans` | ||||||
| const GUILD_CHANNEL = (channelID: string): string => | const GUILD_CHANNEL = (channelID: string): string => | ||||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/channels/${channelID}` |   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/channels/${channelID}` | ||||||
| const GUILD_CHANNELS = (guildID: string, channelID: string): string => | const GUILD_CHANNELS = (guildID: string): string => | ||||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/channels` |   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/channels` | ||||||
| const GUILD_MEMBER = (guildID: string, memberID: string): string => | const GUILD_MEMBER = (guildID: string, memberID: string): string => | ||||||
|   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/members/${memberID}` |   `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/members/${memberID}` | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue