Merge pull request #56 from DjDeveloperr/slash

feat: Slash Commands & Interactions [WIP?]
This commit is contained in:
Helloyunho 2020-12-16 10:53:36 +09:00 committed by GitHub
commit 55ddc64187
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1282 additions and 105 deletions

2
.gitignore vendored
View File

@ -113,3 +113,5 @@ src/test/config.ts
# macOS is shit xD
**/.DS_Store
src/test/music.mp3

43
mod.ts
View File

@ -2,7 +2,9 @@ export { GatewayIntents } from './src/types/gateway.ts'
export { default as EventEmitter } from 'https://deno.land/std@0.74.0/node/events.ts'
export { Base } from './src/structures/base.ts'
export { Gateway } from './src/gateway/index.ts'
export type { ClientEvents } from './src/gateway/handlers/index.ts'
export * from './src/models/client.ts'
export * from './src/models/slashClient.ts'
export { RESTManager } from './src/models/rest.ts'
export * from './src/models/cacheAdapter.ts'
export {
@ -18,6 +20,7 @@ export {
ExtensionCommands,
ExtensionsManager
} from './src/models/extensions.ts'
export { SlashModule } from './src/models/slashModule.ts'
export { CommandClient, command } from './src/models/commandClient.ts'
export type { CommandClientOptions } from './src/models/commandClient.ts'
export { BaseManager } from './src/managers/base.ts'
@ -28,6 +31,8 @@ export { GatewayCache } from './src/managers/gatewayCache.ts'
export { GuildChannelsManager } from './src/managers/guildChannels.ts'
export type { GuildChannel } from './src/managers/guildChannels.ts'
export { GuildManager } from './src/managers/guilds.ts'
export * from './src/structures/slash.ts'
export * from './src/types/slash.ts'
export { GuildEmojisManager } from './src/managers/guildEmojis.ts'
export { MembersManager } from './src/managers/members.ts'
export { MessageReactionsManager } from './src/managers/messageReactions.ts'
@ -80,3 +85,41 @@ export type {
StatusType
} from './src/types/presence.ts'
export { ChannelTypes } from './src/types/channel.ts'
export type { ApplicationPayload } from './src/types/application.ts'
export type { ImageFormats, ImageSize } from './src/types/cdn.ts'
export type {
ChannelMention,
ChannelPayload,
FollowedChannel,
GuildNewsChannelPayload,
GuildChannelCategoryPayload,
GuildChannelPayload,
GuildTextChannelPayload,
GuildVoiceChannelPayload,
GroupDMChannelPayload
} from './src/types/channel.ts'
export type { EmojiPayload } from './src/types/emoji.ts'
export type {
GuildBanPayload,
GuildFeatures,
GuildIntegrationPayload,
GuildPayload
} from './src/types/guild.ts'
export type { InvitePayload, PartialInvitePayload } from './src/types/invite.ts'
export { PermissionFlags } from './src/types/permissionFlags.ts'
export type {
ActivityAssets,
ActivityEmoji,
ActivityFlags,
ActivityParty,
ActivityPayload,
ActivitySecrets,
ActivityTimestamps,
ActivityType
} from './src/types/presence.ts'
export type { RolePayload } from './src/types/role.ts'
export type { TemplatePayload } from './src/types/template.ts'
export type { UserPayload } from './src/types/user.ts'
export { UserFlags } from './src/types/userFlags.ts'
export type { VoiceStatePayload } from './src/types/voice.ts'
export type { WebhookPayload } from './src/types/webhook.ts'

View File

@ -27,7 +27,7 @@ import { webhooksUpdate } from './webhooksUpdate.ts'
import { messageDeleteBulk } from './messageDeleteBulk.ts'
import { userUpdate } from './userUpdate.ts'
import { typingStart } from './typingStart.ts'
import { GuildTextChannel } from '../../structures/textChannel.ts'
import { GuildTextChannel, TextChannel } from '../../structures/textChannel.ts'
import { Guild } from '../../structures/guild.ts'
import { User } from '../../structures/user.ts'
import { Emoji } from '../../structures/emoji.ts'
@ -53,6 +53,8 @@ import {
EveryChannelTypes,
EveryTextChannelTypes
} from '../../utils/getChannelByType.ts'
import { interactionCreate } from './interactionCreate.ts'
import { Interaction } from '../../structures/slash.ts'
export const gatewayHandlers: {
[eventCode in GatewayEvents]: GatewayEventHandler | undefined
@ -93,7 +95,8 @@ export const gatewayHandlers: {
USER_UPDATE: userUpdate,
VOICE_STATE_UPDATE: voiceStateUpdate,
VOICE_SERVER_UPDATE: voiceServerUpdate,
WEBHOOKS_UPDATE: webhooksUpdate
WEBHOOKS_UPDATE: webhooksUpdate,
INTERACTION_CREATE: interactionCreate
}
export interface EventTypes {
@ -107,56 +110,227 @@ export interface VoiceServerUpdateData {
}
export interface ClientEvents extends EventTypes {
/** When Client has successfully connected to Discord */
ready: () => void
/** When a successful reconnect has been made */
reconnect: () => void
/** When a successful session resume has been done */
resumed: () => void
/**
* When a new Channel is created
* @param channel New Channel object
*/
channelCreate: (channel: EveryChannelTypes) => void
/**
* When a Channel was deleted
* @param channel Channel object which was deleted
*/
channelDelete: (channel: EveryChannelTypes) => void
/**
* Channel's Pinned Messages were updated
* @param before Channel object before update
* @param after Channel object after update
*/
channelPinsUpdate: (
before: EveryTextChannelTypes,
after: EveryTextChannelTypes
) => void
/**
* A Channel was updated
* @param before Channel object before update
* @param after Channel object after update
*/
channelUpdate: (before: EveryChannelTypes, after: EveryChannelTypes) => void
/**
* A User was banned from a Guild
* @param guild The Guild from which User was banned
* @param user The User who was banned
*/
guildBanAdd: (guild: Guild, user: User) => void
/**
* A ban from a User in Guild was elevated
* @param guild Guild from which ban was removed
* @param user User of which ban was elevated
*/
guildBanRemove: (guild: Guild, user: User) => void
/**
* Client has joined a new Guild.
* @param guild The new Guild object
*/
guildCreate: (guild: Guild) => void
/**
* A Guild in which Client was either deleted, or bot was kicked
* @param guild The Guild object
*/
guildDelete: (guild: Guild) => void
/**
* A new Emoji was added to Guild
* @param guild Guild in which Emoji was added
* @param emoji The Emoji which was added
*/
guildEmojiAdd: (guild: Guild, emoji: Emoji) => void
/**
* An Emoji was deleted from Guild
* @param guild Guild from which Emoji was deleted
* @param emoji Emoji which was deleted
*/
guildEmojiDelete: (guild: Guild, emoji: Emoji) => void
/**
* An Emoji in a Guild was updated
* @param guild Guild in which Emoji was updated
* @param before Emoji object before update
* @param after Emoji object after update
*/
guildEmojiUpdate: (guild: Guild, before: Emoji, after: Emoji) => void
/**
* Guild's Integrations were updated
* @param guild The Guild object
*/
guildIntegrationsUpdate: (guild: Guild) => void
/**
* A new Member has joined a Guild
* @param member The Member object
*/
guildMemberAdd: (member: Member) => void
/**
* A Guild Member has either left or was kicked from Guild
* @param member The Member object
*/
guildMemberRemove: (member: Member) => void
/**
* A Guild Member was updated. Nickname changed, role assigned, etc.
* @param before Member object before update
* @param after Meber object after update
*/
guildMemberUpdate: (before: Member, after: Member) => void
/**
* A new Role was created in Guild
* @param role The new Role object
*/
guildRoleCreate: (role: Role) => void
/**
* A Role was deleted from the Guild
* @param role The Role object
*/
guildRoleDelete: (role: Role) => void
/**
* A Role was updated in a Guild
* @param before Role object before update
* @param after Role object after updated
*/
guildRoleUpdate: (before: Role, after: Role) => void
/**
* A Guild has been updated. For example name, icon, etc.
* @param before Guild object before update
* @param after Guild object after update
*/
guildUpdate: (before: Guild, after: Guild) => void
/**
* A new Message was created (sent)
* @param message The new Message object
*/
messageCreate: (message: Message) => void
/**
* A Message was deleted.
* @param message The Message object
*/
messageDelete: (message: Message) => void
/**
* Messages were bulk deleted in a Guild Text Channel
* @param channel Channel in which Messages were deleted
* @param messages Collection of Messages deleted
* @param uncached Set of Messages deleted's IDs which were not cached
*/
messageDeleteBulk: (
channel: GuildTextChannel,
messages: Collection<string, Message>,
uncached: Set<string>
) => void
/**
* A Message was updated. For example content, embed, etc.
* @param before Message object before update
* @param after Message object after update
*/
messageUpdate: (before: Message, after: Message) => void
/**
* Reaction was added to a Message
* @param reaction Reaction object
* @param user User who added the reaction
*/
messageReactionAdd: (reaction: MessageReaction, user: User) => void
/**
* Reaction was removed fro a Message
* @param reaction Reaction object
* @param user User to who removed the reaction
*/
messageReactionRemove: (reaction: MessageReaction, user: User) => void
/**
* All reactions were removed from a Message
* @param message Message from which reactions were removed
*/
messageReactionRemoveAll: (message: Message) => void
/**
* All reactions of a single Emoji were removed
* @param message The Message object
* @param emoji The Emoji object
*/
messageReactionRemoveEmoji: (message: Message, emoji: Emoji) => void
/**
* A User has started typing in a Text Channel
*/
typingStart: (
user: User,
channel: EveryChannelTypes,
channel: TextChannel,
at: Date,
guildData?: TypingStartGuildData
) => void
/**
* A new Invite was created
* @param invite New Invite object
*/
inviteCreate: (invite: Invite) => void
/**
* An Invite was deleted
* @param invite Invite object
*/
inviteDelete: (invite: Invite) => void
/**
* A User was updated. For example username, avatar, etc.
* @param before The User object before update
* @param after The User object after update
*/
userUpdate: (before: User, after: User) => void
/**
* Client has received credentials for establishing connection to Voice Server
*/
voiceServerUpdate: (data: VoiceServerUpdateData) => void
/**
* A User has joined a Voice Channel
*/
voiceStateAdd: (state: VoiceState) => void
/**
* A User has left a Voice Channel
*/
voiceStateRemove: (state: VoiceState) => void
/**
* Voice State of a User has been updated
* @param before Voice State object before update
* @param after Voice State object after update
*/
voiceStateUpdate: (state: VoiceState, after: VoiceState) => void
/**
* A User's presence has been updated
* @param presence New Presence
*/
presenceUpdate: (presence: Presence) => void
/**
* Webhooks of a Channel in a Guild has been updated
* @param guild Guild in which Webhooks were updated
* @param channel Channel of which Webhooks were updated
*/
webhooksUpdate: (guild: Guild, channel: GuildTextChannel) => void
/**
* A Slash Command was triggered
*/
interactionCreate: (interaction: Interaction) => void
}

View File

@ -0,0 +1,29 @@
import { Member } from '../../structures/member.ts'
import { Interaction } from '../../structures/slash.ts'
import { GuildTextChannel } from '../../structures/textChannel.ts'
import { InteractionPayload } from '../../types/slash.ts'
import { Gateway, GatewayEventHandler } from '../index.ts'
export const interactionCreate: GatewayEventHandler = async (
gateway: Gateway,
d: InteractionPayload
) => {
const guild = await gateway.client.guilds.get(d.guild_id)
if (guild === undefined) return
await guild.members.set(d.member.user.id, d.member)
const member = ((await guild.members.get(
d.member.user.id
)) as unknown) as Member
const channel =
(await gateway.client.channels.get<GuildTextChannel>(d.channel_id)) ??
(await gateway.client.channels.fetch<GuildTextChannel>(d.channel_id))
const interaction = new Interaction(gateway.client, d, {
member,
guild,
channel
})
gateway.client.emit('interactionCreate', interaction)
}

View File

@ -252,9 +252,9 @@ class Gateway {
const payload: IdentityPayload = {
token: this.token,
properties: {
$os: Deno.build.os,
$browser: 'harmony',
$device: 'harmony'
$os: this.client.clientProperties.os ?? Deno.build.os,
$browser: this.client.clientProperties.browser ?? 'harmony',
$device: this.client.clientProperties.device ?? 'harmony'
},
compress: true,
shard: [0, 1], // TODO: Make sharding possible

View File

@ -12,6 +12,16 @@ import { EmojisManager } from '../managers/emojis.ts'
import { ActivityGame, ClientActivity } from '../types/presence.ts'
import { ClientEvents } from '../gateway/handlers/index.ts'
import { Extension } from './extensions.ts'
import { SlashClient } from './slashClient.ts'
import { Interaction } from '../structures/slash.ts'
import { SlashModule } from './slashModule.ts'
/** OS related properties sent with Gateway Identify */
export interface ClientProperties {
os?: 'darwin' | 'windows' | 'linux' | 'custom_os' | string
browser?: 'harmony' | string
device?: 'harmony' | string
}
/** Some Client Options to modify behaviour */
export interface ClientOptions {
@ -33,6 +43,10 @@ export interface ClientOptions {
reactionCacheLifetime?: number
/** Whether to fetch Uncached Message of Reaction or not? */
fetchUncachedReactions?: boolean
/** Client Properties */
clientProperties?: ClientProperties
/** Enable/Disable Slash Commands Integration (enabled by default) */
enableSlash?: boolean
}
/**
@ -61,6 +75,10 @@ export class Client extends EventEmitter {
reactionCacheLifetime: number = 3600000
/** Whether to fetch Uncached Message of Reaction or not? */
fetchUncachedReactions: boolean = false
/** Client Properties */
clientProperties: ClientProperties
/** Slash-Commands Management client */
slash: SlashClient
users: UsersManager = new UsersManager(this)
guilds: GuildManager = new GuildManager(this)
@ -72,6 +90,13 @@ export class Client extends EventEmitter {
/** Client's presence. Startup one if set before connecting */
presence: ClientPresence = new ClientPresence()
_decoratedEvents?: { [name: string]: (...args: any[]) => any }
_decoratedSlash?: Array<{
name: string
guild?: string
handler: (interaction: Interaction) => any
}>
_decoratedSlashModules?: SlashModule[]
private readonly _untypedOn = this.on
@ -113,6 +138,19 @@ export class Client extends EventEmitter {
})
this._decoratedEvents = undefined
}
this.clientProperties =
options.clientProperties === undefined
? {
os: Deno.build.os,
browser: 'harmony',
device: 'harmony'
}
: options.clientProperties
this.slash = new SlashClient(this, {
enabled: options.enableSlash
})
}
/**
@ -171,7 +209,34 @@ export function event(name?: string) {
const listener = ((client as unknown) as {
[name: string]: (...args: any[]) => any
})[prop]
if (typeof listener !== 'function')
throw new Error('@event decorator requires a function')
if (client._decoratedEvents === undefined) client._decoratedEvents = {}
client._decoratedEvents[name === undefined ? prop : name] = listener
}
}
export function slash(name?: string, guild?: string) {
return function (client: Client | SlashModule, prop: string) {
if (client._decoratedSlash === undefined) client._decoratedSlash = []
const item = (client as { [name: string]: any })[prop]
if (typeof item !== 'function') {
client._decoratedSlash.push(item)
} else
client._decoratedSlash.push({
name: name ?? prop,
guild,
handler: item
})
}
}
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)
}
}

View File

@ -383,18 +383,26 @@ export class CommandClient extends Client implements CommandClientOptions {
export function command(options?: CommandOptions) {
return function (target: CommandClient | Extension, name: string) {
if (target._decoratedCommands === undefined) target._decoratedCommands = {}
const prop = ((target as unknown) as {
[name: string]: (ctx: CommandContext) => any
})[name]
if (prop instanceof Command) {
target._decoratedCommands[prop.name] = prop
return
}
const command = new Command()
command.name = name
command.execute = ((target as unknown) as {
[name: string]: (ctx: CommandContext) => any
})[name]
command.execute = prop
if (options !== undefined) Object.assign(command, options)
if (target instanceof Extension) command.extension = target
if (target._decoratedCommands === undefined) target._decoratedCommands = {}
target._decoratedCommands[command.name] = command
}
}

View File

@ -277,11 +277,11 @@ export class RESTManager {
message: body?.message,
errors: Object.fromEntries(
Object.entries(
body?.errors as {
(body?.errors as {
[name: string]: {
_errors: Array<{ code: string; message: string }>
}
}
}) ?? {}
).map((entry) => {
return [entry[0], entry[1]._errors]
})

222
src/models/slashClient.ts Normal file
View File

@ -0,0 +1,222 @@
import { Guild } from '../structures/guild.ts'
import { Interaction } from '../structures/slash.ts'
import {
APPLICATION_COMMAND,
APPLICATION_COMMANDS,
APPLICATION_GUILD_COMMAND,
APPLICATION_GUILD_COMMANDS
} from '../types/endpoint.ts'
import {
InteractionType,
SlashCommandOption,
SlashCommandPartial,
SlashCommandPayload
} from '../types/slash.ts'
import { Collection } from '../utils/collection.ts'
import { Client } from './client.ts'
export interface SlashOptions {
enabled?: boolean
}
export class SlashCommand {
slash: SlashCommandsManager
id: string
applicationID: string
name: string
description: string
options: SlashCommandOption[]
_guild?: string
constructor(manager: SlashCommandsManager, data: SlashCommandPayload) {
this.slash = manager
this.id = data.id
this.applicationID = data.application_id
this.name = data.name
this.description = data.description
this.options = data.options
}
async delete(): Promise<void> {
await this.slash.delete(this.id, this._guild)
}
async edit(data: SlashCommandPartial): Promise<void> {
await this.slash.edit(this.id, data, this._guild)
}
}
export class SlashCommandsManager {
client: Client
slash: SlashClient
constructor(client: Client) {
this.client = client
this.slash = client.slash
}
/** Get all Global Slash Commands */
async all(): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>()
const res = (await this.client.rest.get(
APPLICATION_COMMANDS(this.client.user?.id as string)
)) as SlashCommandPayload[]
if (!Array.isArray(res)) return col
for (const raw of res) {
const cmd = new SlashCommand(this, raw)
col.set(raw.id, cmd)
}
return col
}
/** Get a Guild's Slash Commands */
async guild(
guild: Guild | string
): Promise<Collection<string, SlashCommand>> {
const col = new Collection<string, SlashCommand>()
const res = (await this.client.rest.get(
APPLICATION_GUILD_COMMANDS(
this.client.user?.id as string,
typeof guild === 'string' ? guild : guild.id
)
)) as SlashCommandPayload[]
if (!Array.isArray(res)) return col
for (const raw of res) {
const cmd = new SlashCommand(this, raw)
cmd._guild = typeof guild === 'string' ? guild : guild.id
col.set(raw.id, cmd)
}
return col
}
/** Create a Slash Command (global or Guild) */
async create(
data: SlashCommandPartial,
guild?: Guild | string
): Promise<SlashCommand> {
const payload = await this.client.rest.post(
guild === undefined
? APPLICATION_COMMANDS(this.client.user?.id as string)
: APPLICATION_GUILD_COMMANDS(
this.client.user?.id as string,
typeof guild === 'string' ? guild : guild.id
),
data
)
const cmd = new SlashCommand(this, payload)
cmd._guild =
typeof guild === 'string' || guild === undefined ? guild : guild.id
return cmd
}
/** Edit a Slash Command (global or Guild) */
async edit(
id: string,
data: SlashCommandPartial,
guild?: Guild | string
): Promise<SlashCommandsManager> {
await this.client.rest.patch(
guild === undefined
? APPLICATION_COMMAND(this.client.user?.id as string, id)
: APPLICATION_GUILD_COMMAND(
this.client.user?.id as string,
typeof guild === 'string' ? guild : guild.id,
id
),
data
)
return this
}
/** Delete a Slash Command (global or Guild) */
async delete(
id: string,
guild?: Guild | string
): Promise<SlashCommandsManager> {
await this.client.rest.delete(
guild === undefined
? APPLICATION_COMMAND(this.client.user?.id as string, id)
: APPLICATION_GUILD_COMMAND(
this.client.user?.id as string,
typeof guild === 'string' ? guild : guild.id,
id
)
)
return this
}
}
export type SlashCommandHandlerCallback = (interaction: Interaction) => any
export interface SlashCommandHandler {
name: string
guild?: string
handler: SlashCommandHandlerCallback
}
export class SlashClient {
client: Client
enabled: boolean = true
commands: SlashCommandsManager
handlers: SlashCommandHandler[] = []
constructor(client: Client, options?: SlashOptions) {
this.client = client
this.commands = new SlashCommandsManager(client)
if (options !== undefined) {
this.enabled = options.enabled ?? true
}
if (this.client._decoratedSlash !== undefined) {
this.client._decoratedSlash.forEach((e) => {
this.handlers.push(e)
})
}
this.client.on('interactionCreate', (interaction) =>
this.process(interaction)
)
}
/** Adds a new Slash Command Handler */
handle(
name: string,
handler: SlashCommandHandlerCallback,
guild?: string
): SlashClient {
this.handlers.push({
name,
guild,
handler
})
return this
}
/** Process an incoming Slash Command (interaction) */
private process(interaction: Interaction): void {
if (!this.enabled) return
if (interaction.type !== InteractionType.APPLICATION_COMMAND) return
let cmd
if (interaction.guild !== undefined)
cmd =
this.handlers.find(
(e) => e.guild !== undefined && e.name === interaction.name
) ?? this.handlers.find((e) => e.name === interaction.name)
else cmd = this.handlers.find((e) => e.name === interaction.name)
if (cmd === undefined) return
cmd.handler(interaction)
}
}

18
src/models/slashModule.ts Normal file
View File

@ -0,0 +1,18 @@
import { SlashCommandHandler } from './slashClient.ts'
export class SlashModule {
name: string = ''
commands: SlashCommandHandler[] = []
_decoratedSlash?: SlashCommandHandler[]
constructor() {
if (this._decoratedSlash !== undefined) {
this.commands = this._decoratedSlash
}
}
add(handler: SlashCommandHandler): SlashModule {
this.commands.push(handler)
return this
}
}

View File

@ -50,17 +50,23 @@ export class Presence extends Base {
}
}
interface StatusPayload extends StatusUpdatePayload {
client_status?: ClientStatus
}
export class ClientPresence {
status: StatusType = 'online'
activity?: ActivityGame | ActivityGame[]
since?: number | null
afk?: boolean
clientStatus?: ClientStatus
constructor(data?: ClientActivity | StatusUpdatePayload | ActivityGame) {
constructor(data?: ClientActivity | StatusPayload | ActivityGame) {
if (data !== undefined) {
if ((data as ClientActivity).activity !== undefined) {
Object.assign(this, data)
} else if ((data as StatusUpdatePayload).activities !== undefined) {
} else if ((data as StatusPayload).activities !== undefined) {
this.parse(data as StatusPayload)
} else if ((data as ActivityGame).name !== undefined) {
if (this.activity === undefined) {
this.activity = data as ActivityGame
@ -71,11 +77,12 @@ export class ClientPresence {
}
}
parse(payload: StatusUpdatePayload): ClientPresence {
parse(payload: StatusPayload): ClientPresence {
this.afk = payload.afk
this.activity = payload.activities ?? undefined
this.since = payload.since
this.status = payload.status
// this.clientStatus = payload.client_status
return this
}
@ -83,12 +90,13 @@ export class ClientPresence {
return new ClientPresence().parse(payload)
}
create(): StatusUpdatePayload {
create(): StatusPayload {
return {
afk: this.afk === undefined ? false : this.afk,
activities: this.createActivity(),
since: this.since === undefined ? null : this.since,
status: this.status === undefined ? 'online' : this.status
// client_status: this.clientStatus
}
}
@ -144,4 +152,13 @@ export class ClientPresence {
this.since = since
return this
}
// setClientStatus(
// client: 'desktop' | 'web' | 'mobile',
// status: StatusType
// ): ClientPresence {
// if (this.clientStatus === undefined) this.clientStatus = {}
// this.clientStatus[client] = status
// return this
// }
}

233
src/structures/slash.ts Normal file
View File

@ -0,0 +1,233 @@
import { Client } from '../models/client.ts'
import { MessageOption } from '../types/channel.ts'
import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts'
import {
InteractionData,
InteractionPayload,
InteractionResponsePayload,
InteractionResponseType
} from '../types/slash.ts'
import { Embed } from './embed.ts'
import { Guild } from './guild.ts'
import { Member } from './member.ts'
import { Message } from './message.ts'
import { GuildTextChannel, TextChannel } from './textChannel.ts'
import { User } from './user.ts'
import { Webhook } from './webhook.ts'
interface WebhookMessageOptions extends MessageOption {
embeds?: Embed[]
name?: string
avatar?: string
}
type AllWebhookMessageOptions = string | WebhookMessageOptions
export interface InteractionResponse {
type?: InteractionResponseType
content?: string
embeds?: Embed[]
tts?: boolean
flags?: number
temp?: boolean
allowedMentions?: {
parse?: string
roles?: string[]
users?: string[]
everyone?: boolean
}
}
export class Interaction {
client: Client
type: number
token: string
id: string
data: InteractionData
channel: GuildTextChannel
guild: Guild
member: Member
_savedHook?: Webhook
constructor(
client: Client,
data: InteractionPayload,
others: {
channel: GuildTextChannel
guild: Guild
member: Member
}
) {
this.client = client
this.type = data.type
this.token = data.token
this.member = others.member
this.id = data.id
this.data = data.data
this.guild = others.guild
this.channel = others.channel
}
get user(): User {
return this.member.user
}
get name(): string {
return this.data.name
}
option<T = any>(name: string): T {
return this.data.options.find((e) => e.name === name)?.value
}
async respond(data: InteractionResponse): Promise<Interaction> {
const payload: InteractionResponsePayload = {
type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data:
data.type === undefined ||
data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE ||
data.type === InteractionResponseType.CHANNEL_MESSAGE
? {
content: data.content ?? '',
embeds: data.embeds,
tts: data.tts ?? false,
flags: data.temp === true ? 64 : data.flags ?? undefined,
allowed_mentions: (data.allowedMentions ?? undefined) as any
}
: undefined
}
await this.client.rest.post(
INTERACTION_CALLBACK(this.id, this.token),
payload
)
return this
}
async editResponse(data: {
content?: string
embeds?: Embed[]
}): Promise<Interaction> {
const url = WEBHOOK_MESSAGE(
this.client.user?.id as string,
this.token,
'@original'
)
await this.client.rest.patch(url, {
content: data.content ?? '',
embeds: data.embeds ?? []
})
return this
}
async deleteResponse(): Promise<Interaction> {
const url = WEBHOOK_MESSAGE(
this.client.user?.id as string,
this.token,
'@original'
)
await this.client.rest.delete(url)
return this
}
get url(): string {
return `https://discord.com/api/v8/webhooks/${this.client.user?.id}/${this.token}`
}
async send(
text?: string | AllWebhookMessageOptions,
option?: AllWebhookMessageOptions
): Promise<Message> {
if (typeof text === 'object') {
option = text
text = undefined
}
if (text === undefined && option === undefined) {
throw new Error('Either text or option is necessary.')
}
if (option instanceof Embed)
option = {
embeds: [option]
}
const payload: any = {
content: text,
embeds:
(option as WebhookMessageOptions)?.embed !== undefined
? [(option as WebhookMessageOptions).embed]
: (option as WebhookMessageOptions)?.embeds !== undefined
? (option as WebhookMessageOptions).embeds
: undefined,
file: (option as WebhookMessageOptions)?.file,
tts: (option as WebhookMessageOptions)?.tts,
allowed_mentions: (option as WebhookMessageOptions)?.allowedMentions
}
if ((option as WebhookMessageOptions)?.name !== undefined) {
payload.username = (option as WebhookMessageOptions)?.name
}
if ((option as WebhookMessageOptions)?.avatar !== undefined) {
payload.avatar = (option as WebhookMessageOptions)?.avatar
}
if (
payload.embeds !== undefined &&
payload.embeds instanceof Array &&
payload.embeds.length > 10
)
throw new Error(
`Cannot send more than 10 embeds through Interaction Webhook`
)
const resp = await this.client.rest.post(`${this.url}?wait=true`, payload)
const res = new Message(
this.client,
resp,
(this as unknown) as TextChannel,
(this as unknown) as User
)
await res.mentions.fromPayload(resp)
return res
}
async editMessage(
msg: Message | string,
data: {
content?: string
embeds?: Embed[]
file?: any
allowed_mentions?: {
parse?: string
roles?: string[]
users?: string[]
everyone?: boolean
}
}
): Promise<Interaction> {
await this.client.rest.patch(
WEBHOOK_MESSAGE(
this.client.user?.id as string,
this.token ?? this.client.token,
typeof msg === 'string' ? msg : msg.id
),
data
)
return this
}
async deleteMessage(msg: Message | string): Promise<Interaction> {
await this.client.rest.delete(
WEBHOOK_MESSAGE(
this.client.user?.id as string,
this.token ?? this.client.token,
typeof msg === 'string' ? msg : msg.id
)
)
return this
}
}

View File

@ -15,6 +15,8 @@ export class VoiceState extends Base {
sessionID: string
deaf: boolean
mute: boolean
selfDeaf: boolean
selfMute: boolean
stream?: boolean
video: boolean
suppress: boolean
@ -38,8 +40,8 @@ export class VoiceState extends Base {
this.guild = _data.guild
this.deaf = data.deaf
this.mute = data.mute
this.deaf = data.self_deaf
this.mute = data.self_mute
this.selfDeaf = data.self_deaf
this.selfMute = data.self_mute
this.stream = data.self_stream
this.video = data.self_video
this.suppress = data.suppress
@ -52,6 +54,8 @@ export class VoiceState extends Base {
this.mute = data.mute ?? this.mute
this.deaf = data.self_deaf ?? this.deaf
this.mute = data.self_mute ?? this.mute
this.selfDeaf = data.self_deaf ?? this.selfDeaf
this.selfMute = data.self_mute ?? this.selfMute
this.stream = data.self_stream ?? this.stream
this.video = data.self_video ?? this.video
this.suppress = data.suppress ?? this.suppress

View File

@ -12,6 +12,7 @@ import { Message } from './message.ts'
import { TextChannel } from './textChannel.ts'
import { User } from './user.ts'
import { fetchAuto } from 'https://raw.githubusercontent.com/DjDeveloperr/fetch-base64/main/mod.ts'
import { WEBHOOK_MESSAGE } from '../types/endpoint.ts'
export interface WebhookMessageOptions extends MessageOption {
embeds?: Embed[]
@ -191,4 +192,40 @@ export class Webhook {
if (resp.response.status !== 204) return false
else return true
}
async editMessage(
message: string | Message,
data: {
content?: string
embeds?: Embed[]
file?: any
allowed_mentions?: {
parse?: string
roles?: string[]
users?: string[]
everyone?: boolean
}
}
): Promise<Webhook> {
await this.client?.rest.patch(
WEBHOOK_MESSAGE(
this.id,
(this.token ?? this.client.token) as string,
typeof message === 'string' ? message : message.id
),
data
)
return this
}
async deleteMessage(message: string | Message): Promise<Webhook> {
await this.client?.rest.delete(
WEBHOOK_MESSAGE(
this.id,
(this.token ?? this.client.token) as string,
typeof message === 'string' ? message : message.id
)
)
return this
}
}

View File

@ -2,7 +2,6 @@ import {
Client,
Intents,
Message,
ClientPresence,
Member,
Role,
GuildChannel,
@ -15,10 +14,9 @@ import {
import { TOKEN } from './config.ts'
const client = new Client({
presence: new ClientPresence({
name: 'Pokémon Sword',
type: 'COMPETING'
})
clientProperties: {
browser: 'Discord iOS'
}
// bot: false,
// cache: new RedisCacheAdapter({
// hostname: '127.0.0.1',

137
src/test/music.ts Normal file
View File

@ -0,0 +1,137 @@
import {
CommandClient,
event,
Intents,
command,
CommandContext,
Extension,
Collection
} from '../../mod.ts'
import { LL_IP, LL_PASS, LL_PORT, TOKEN } from './config.ts'
import {
Manager,
Player
} from 'https://raw.githubusercontent.com/DjDeveloperr/lavaclient-deno/master/mod.ts'
export const nodes = [
{
id: 'main',
host: LL_IP,
port: LL_PORT,
password: LL_PASS
}
]
class MyClient extends CommandClient {
manager: Manager
constructor() {
super({
prefix: ['.'],
caseSensitive: false
})
// eslint-disable-next-line @typescript-eslint/no-this-alias
const client = this
this.manager = new Manager(nodes, {
send(id, payload) {
// Sharding not added yet
client.gateway?.send(payload)
}
})
this.manager.on('socketError', ({ id }, error) =>
console.error(`${id} ran into an error`, error)
)
this.manager.on('socketReady', (node) =>
console.log(`${node.id} connected.`)
)
this.on('raw', (evt: string, d: any) => {
if (evt === 'VOICE_SERVER_UPDATE') this.manager.serverUpdate(d)
else if (evt === 'VOICE_STATE_UPDATE') this.manager.stateUpdate(d)
})
}
@event()
ready(): void {
console.log(`Logged in as ${this.user?.tag}!`)
this.manager.init(this.user?.id as string)
}
}
const players = new Collection<string, Player>()
class VCExtension extends Extension {
name = 'VC'
subPrefix = 'vc'
@command()
async join(ctx: CommandContext): Promise<any> {
if (players.has(ctx.guild?.id as string) === true)
return ctx.message.reply(`Already playing in this server!`)
ctx.argString = ctx.argString.slice(4).trim()
if (ctx.argString === '')
return ctx.message.reply('You gave nothing to search.')
const userVS = await ctx.guild?.voiceStates.get(ctx.author.id)
if (userVS === undefined) {
ctx.message.reply("You're not in VC.")
return
}
const player = (ctx.client as MyClient).manager.create(
ctx.guild?.id as string
)
await player.connect(userVS.channel?.id as string, { selfDeaf: true })
ctx.message.reply(`Joined VC channel - ${userVS.channel?.name}!`)
players.set(ctx.guild?.id as string, player)
ctx.channel.send(`Loading...`)
ctx.channel.send(`Searching for ${ctx.argString}...`)
const { track, info } = await player.manager
.search(`ytsearch:${ctx.argString}`)
.then((e) => e.tracks[0])
await player.play(track)
ctx.channel.send(`Now playing ${info.title}!`)
}
@command()
async leave(ctx: CommandContext): Promise<any> {
const userVS = await ctx.guild?.voiceStates.get(
(ctx.client.user?.id as unknown) as string
)
if (userVS === undefined) {
ctx.message.reply("I'm not in VC.")
return
}
userVS.channel?.leave()
ctx.message.reply(`Left VC channel - ${userVS.channel?.name}!`)
if (players.has(ctx.guild?.id as string) !== true)
return ctx.message.reply('Not playing anything in this server.')
const player = (players.get(ctx.guild?.id as string) as unknown) as Player
await player.stop()
await player.destroy()
players.delete(ctx.guild?.id as string)
ctx.message.reply('Stopped player')
}
}
const client = new MyClient()
client.extensions.load(VCExtension)
client.connect(TOKEN, Intents.None)

51
src/test/slash-cmd.ts Normal file
View File

@ -0,0 +1,51 @@
import { TOKEN } from './config.ts'
export const CMD = {
name: 'blep',
description: 'Send a random adorable animal photo',
options: [
{
name: 'animal',
description: 'The type of animal',
type: 3,
required: true,
choices: [
{
name: 'Dog',
value: 'animal_dog'
},
{
name: 'Cat',
value: 'animal_dog'
},
{
name: 'Penguin',
value: 'animal_penguin'
}
]
},
{
name: 'only_smol',
description: 'Whether to show only baby animals',
type: 5,
required: false
}
]
}
// fetch('https://discord.com/api/v8/applications/783937840752099332/commands', {
fetch(
'https://discord.com/api/v8/applications/783937840752099332/guilds/783319033205751809/commands',
{
method: 'POST',
body: JSON.stringify(CMD),
headers: {
'Content-Type': 'application/json',
Authorization:
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
'Bot ' + TOKEN
}
}
)
.then((r) => r.json())
.then(console.log)

96
src/test/slash.ts Normal file
View File

@ -0,0 +1,96 @@
import { Client, Intents, event, slash } from '../../mod.ts'
import { Embed } from '../structures/embed.ts'
import { Interaction } from '../structures/slash.ts'
import { TOKEN } from './config.ts'
export class MyClient extends Client {
@event()
ready(): void {
console.log(`Logged in as ${this.user?.tag}!`)
}
@slash()
send(d: Interaction): void {
d.respond({
content: d.data.options.find((e) => e.name === 'content')?.value
})
}
@slash()
async eval(d: Interaction): Promise<void> {
if (
d.user.id !== '422957901716652033' &&
d.user.id !== '682849186227552266'
) {
d.respond({
content: 'This command can only be used by owner!'
})
} else {
const code = d.data.options.find((e) => e.name === 'code')
?.value as string
try {
// eslint-disable-next-line no-eval
let evaled = eval(code)
if (evaled instanceof Promise) evaled = await evaled
if (typeof evaled === 'object') evaled = Deno.inspect(evaled)
let res = `${evaled}`.substring(0, 1990)
while (client.token !== undefined && res.includes(client.token)) {
res = res.replace(client.token, '[REMOVED]')
}
d.respond({
content: '```js\n' + `${res}` + '\n```'
}).catch(() => {})
} catch (e) {
d.respond({
content: '```js\n' + `${e.stack}` + '\n```'
})
}
}
}
@slash()
async hug(d: Interaction): Promise<void> {
const id = d.data.options.find((e) => e.name === 'user')?.value as string
const user = (await client.users.get(id)) ?? (await client.users.fetch(id))
const url = await fetch('https://nekos.life/api/v2/img/hug')
.then((r) => r.json())
.then((e) => e.url)
d.respond({
embeds: [
new Embed()
.setTitle(`${d.user.username} hugged ${user?.username}!`)
.setImage({ url })
.setColor(0x2f3136)
]
})
}
@slash()
async kiss(d: Interaction): Promise<void> {
const id = d.data.options.find((e) => e.name === 'user')?.value as string
const user = (await client.users.get(id)) ?? (await client.users.fetch(id))
const url = await fetch('https://nekos.life/api/v2/img/kiss')
.then((r) => r.json())
.then((e) => e.url)
d.respond({
embeds: [
new Embed()
.setTitle(`${d.user.username} kissed ${user?.username}!`)
.setImage({ url })
.setColor(0x2f3136)
]
})
}
@slash('ping')
pingCmd(d: Interaction): void {
d.respond({
content: `Pong!`
})
}
}
const client = new MyClient()
client.connect(TOKEN, Intents.None)

View File

@ -191,81 +191,35 @@ const VOICE_REGIONS = (guildID: string): string =>
const CLIENT_USER = (): string =>
`${DISCORD_API_URL}/v${DISCORD_API_VERSION}/users/@me`
export default [
GUILDS,
GUILD,
GUILD_AUDIT_LOGS,
GUILD_WIDGET,
GUILD_EMOJI,
GUILD_ROLE,
GUILD_ROLES,
GUILD_INTEGRATION,
GUILD_INTEGRATIONS,
GUILD_INTEGARTION_SYNC,
GUILD_WIDGET_IMAGE,
GUILD_BAN,
GUILD_BANS,
GUILD_CHANNEL,
GUILD_CHANNELS,
GUILD_MEMBER,
CLIENT_USER,
GUILD_MEMBERS,
GUILD_MEMBER_ROLE,
GUILD_INVITES,
GUILD_LEAVE,
GUILD_PRUNE,
GUILD_VANITY_URL,
GUILD_NICK,
GUILD_PREVIEW,
CHANNEL,
CHANNELS,
CHANNEL_MESSAGE,
CHANNEL_MESSAGES,
CHANNEL_CROSSPOST,
MESSAGE_REACTIONS,
MESSAGE_REACTION,
MESSAGE_REACTION_ME,
MESSAGE_REACTION_USER,
CHANNEL_BULK_DELETE,
CHANNEL_FOLLOW,
CHANNEL_INVITES,
CHANNEL_PIN,
CHANNEL_PINS,
CHANNEL_PERMISSION,
CHANNEL_TYPING,
GROUP_RECIPIENT,
CURRENT_USER,
CURRENT_USER_GUILDS,
USER_DM,
USER_CONNECTIONS,
LEAVE_GUILD,
USER,
CHANNEL_WEBHOOKS,
GUILD_WEBHOOK,
WEBHOOK,
WEBHOOK_WITH_TOKEN,
SLACK_WEBHOOK,
GITHUB_WEBHOOK,
GATEWAY,
GATEWAY_BOT,
CUSTOM_EMOJI,
GUILD_ICON,
GUILD_SPLASH,
GUILD_DISCOVERY_SPLASH,
GUILD_BANNER,
DEFAULT_USER_AVATAR,
USER_AVATAR,
APPLICATION_ASSET,
ACHIEVEMENT_ICON,
TEAM_ICON,
EMOJI,
GUILD_EMOJIS,
TEMPLATE,
INVITE,
VOICE_REGIONS
]
const APPLICATION_COMMANDS = (id: string): string =>
`${DISCORD_API_URL}/v${DISCORD_API_VERSION}/applications/${id}/commands`
const APPLICATION_COMMAND = (id: string, cmdID: string): string =>
`${DISCORD_API_URL}/v${DISCORD_API_VERSION}/applications/${id}/commands/${cmdID}`
const APPLICATION_GUILD_COMMANDS = (id: string, guildID: string): string =>
`${DISCORD_API_URL}/v${DISCORD_API_VERSION}/applications/${id}/guilds/${guildID}/commands`
const APPLICATION_GUILD_COMMAND = (
id: string,
guildID: string,
cmdID: string
): string =>
`${DISCORD_API_URL}/v${DISCORD_API_VERSION}/applications/${id}/guilds/${guildID}/commands/${cmdID}`
const WEBHOOK_MESSAGE = (id: string, token: string, msgID: string): string =>
`${DISCORD_API_URL}/v${DISCORD_API_VERSION}/webhooks/${id}/${token}/messages/${msgID}`
const INTERACTION_CALLBACK = (id: string, token: string): string =>
`${DISCORD_API_URL}/v${DISCORD_API_VERSION}/interactions/${id}/${token}/callback`
export {
INTERACTION_CALLBACK,
APPLICATION_COMMAND,
APPLICATION_GUILD_COMMAND,
WEBHOOK_MESSAGE,
APPLICATION_COMMANDS,
APPLICATION_GUILD_COMMANDS,
GUILDS,
GUILD,
GUILD_AUDIT_LOGS,

View File

@ -105,7 +105,8 @@ export enum GatewayEvents {
User_Update = 'USER_UPDATE',
Voice_Server_Update = 'VOICE_SERVER_UPDATE',
Voice_State_Update = 'VOICE_STATE_UPDATE',
Webhooks_Update = 'WEBHOOKS_UPDATE'
Webhooks_Update = 'WEBHOOKS_UPDATE',
Interaction_Create = 'INTERACTION_CREATE'
}
export interface IdentityPayload {
@ -120,11 +121,11 @@ export interface IdentityPayload {
}
export interface IdentityConnection {
$os: 'darwin' | 'windows' | 'linux' | 'custom os'
$browser: 'harmony' | 'Firefox'
$device: 'harmony' | ''
$referrer?: ''
$referring_domain?: ''
$os: 'darwin' | 'windows' | 'linux' | 'custom os' | string
$browser: 'harmony' | 'Firefox' | string
$device: 'harmony' | string
$referrer?: '' | string
$referring_domain?: '' | string
}
export interface Resume {

90
src/types/slash.ts Normal file
View File

@ -0,0 +1,90 @@
import { EmbedPayload } from './channel.ts'
import { MemberPayload } from './guild.ts'
export interface InteractionOption {
name: string
value?: any
options?: any[]
}
export interface InteractionData {
name: string
id: string
options: InteractionOption[]
}
export enum InteractionType {
PING = 1,
APPLICATION_COMMAND = 2
}
export interface InteractionPayload {
type: InteractionType
token: string
member: MemberPayload
id: string
data: InteractionData
guild_id: string
channel_id: string
}
export interface SlashCommandChoice {
name: string
value: string
}
export enum SlashCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
INTEGER = 4,
BOOLEAN = 5,
USER = 6,
CHANNEL = 7,
ROLE = 8
}
export interface SlashCommandOption {
name: string
description: string
type: SlashCommandOptionType
required: boolean
choices?: SlashCommandChoice[]
options?: SlashCommandOption[]
}
export interface SlashCommandPartial {
name: string
description: string
options: SlashCommandOption[]
}
export interface SlashCommandPayload extends SlashCommandPartial {
id: string
application_id: string
}
export enum InteractionResponseType {
PONG = 1,
ACKNOWLEDGE = 2,
CHANNEL_MESSAGE = 3,
CHANNEL_MESSAGE_WITH_SOURCE = 4,
ACK_WITH_SOURCE = 5
}
export interface InteractionResponsePayload {
type: InteractionResponseType
data?: InteractionResponseDataPayload
}
export interface InteractionResponseDataPayload {
tts?: boolean
content: string
embeds?: EmbedPayload[]
allowed_mentions?: {
parse?: 'everyone' | 'users' | 'roles'
roles?: string[]
users?: string[]
}
flags?: number
}

View File

@ -1,7 +1,6 @@
export const UserFlags = {
DISCORD_EMPLOYEE: 1 << 0,
PARTNERED_SERVER_OWNER: 1 << 1,
DISCORD_PARTNER: 1 << 1,
HYPESQUAD_EVENTS: 1 << 2,
BUGHUNTER_LEVEL_1: 1 << 3,
HOUSE_BRAVERY: 1 << 6,

View File

@ -1,7 +1,6 @@
// https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice
import { MemberPayload } from './guild.ts'
export enum VoiceOpcodes { // add VoiceOpcodes - UnderC -
export enum VoiceOpcodes {
IDENTIFY = 0,
SELECT_PROTOCOL = 1,
READY = 2,