diff --git a/README.md b/README.md index 9ac5982..52f1bc9 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ client.on('messageCreate', (msg: Message): void => { }) // Connect to gateway -// Replace with your bot's token and intents (Intents.All, Intents.Presence, Intents.GuildMembers) +// Replace with your bot's token and intents (Intents.All, Intents.None, Intents.Presence, Intents.GuildMembers) client.connect('super secret token comes here', Intents.All) ``` @@ -84,7 +84,7 @@ class PingCommand extends Command { client.commands.add(PingCommand) // Connect to gateway -// Replace with your bot's token and intents (Intents.All, Intents.Presence, Intents.GuildMembers) +// Replace with your bot's token and intents (Intents.All, Intents.None, Intents.Presence, Intents.GuildMembers) client.connect('super secret token comes here', Intents.All) ``` diff --git a/mod.ts b/mod.ts index f9881e5..9edcc67 100644 --- a/mod.ts +++ b/mod.ts @@ -26,7 +26,6 @@ export * from './src/structures/groupChannel.ts' export * from './src/structures/guild.ts' export * from './src/structures/guildCategoryChannel.ts' export * from './src/structures/guildNewsChannel.ts' -export * from './src/structures/guildTextChannel.ts' export * from './src/structures/guildVoiceChannel.ts' export * from './src/structures/invite.ts' export * from './src/structures/member.ts' diff --git a/src/gateway/handlers/messageCreate.ts b/src/gateway/handlers/messageCreate.ts index 3f43aa5..0a73bba 100644 --- a/src/gateway/handlers/messageCreate.ts +++ b/src/gateway/handlers/messageCreate.ts @@ -1,5 +1,4 @@ import { Message } from '../../structures/message.ts' -import { MessageMentions } from '../../structures/messageMentions.ts' import { TextChannel } from '../../structures/textChannel.ts' import { User } from '../../structures/user.ts' import { MessagePayload } from '../../types/channel.ts' @@ -26,10 +25,10 @@ export const messageCreate: GatewayEventHandler = async ( await guild.members.set(d.author.id, d.member) member = await guild.members.get(d.author.id) } - const mentions = new MessageMentions() - const message = new Message(gateway.client, d, channel as any, user, mentions) - message.member = member + const message = new Message(gateway.client, d, channel as any, user) if (guild !== undefined) message.guild = guild + await message.mentions.fromPayload(d) + message.member = member if (message.member !== undefined) { if (message.member.user === undefined) { const user = await gateway.client.users.get(message.member.id) diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 7a930a3..ac9e0b8 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -14,6 +14,7 @@ import { gatewayHandlers } from './handlers/index.ts' import { GATEWAY_BOT } from '../types/endpoint.ts' import { GatewayCache } from '../managers/gatewayCache.ts' import { ClientActivityPayload } from '../structures/presence.ts' +import { delay } from "../utils/delay.ts" /** * Handles Discord gateway connection. @@ -140,7 +141,7 @@ class Gateway { } } - private onclose (event: CloseEvent): void { + private async onclose (event: CloseEvent): Promise { this.debug(`Connection Closed with code: ${event.code}`) if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) { @@ -178,7 +179,8 @@ class Gateway { } else if (event.code === GatewayCloseCodes.DISALLOWED_INTENTS) { throw new Error("Given Intents aren't allowed") } else { - this.debug('Unknown Close code, probably connection error. Reconnecting.') + this.debug('Unknown Close code, probably connection error. Reconnecting in 5s.') + await delay(5000) // eslint-disable-next-line @typescript-eslint/no-floating-promises this.reconnect() } @@ -219,7 +221,7 @@ class Gateway { token: this.token, properties: { $os: Deno.build.os, - $browser: 'harmony', // TODO: Change lib name + $browser: 'harmony', $device: 'harmony' }, compress: true, @@ -233,18 +235,17 @@ class Gateway { } if (this.client.bot === false) { - // TODO: Complete Selfbot support this.debug('Modify Identify Payload for Self-bot..') - // delete payload.d['intents'] - // payload.d.intents = Intents.None + delete payload.d.intents payload.d.presence = null payload.d.properties = { $os: 'Windows', $browser: 'Firefox', - $device: '' + $device: '', + $referrer: '', + $referring_domain: '' } - - this.debug('Warn: Support for selfbots is incomplete') + payload.d.synced_guilds = [] } this.send(payload) diff --git a/src/managers/guildChannels.ts b/src/managers/guildChannels.ts index 69735ac..da3e375 100644 --- a/src/managers/guildChannels.ts +++ b/src/managers/guildChannels.ts @@ -2,7 +2,7 @@ import { Client } from '../models/client.ts' import { Channel } from '../structures/channel.ts' import { Guild } from '../structures/guild.ts' import { CategoryChannel } from '../structures/guildCategoryChannel.ts' -import { GuildTextChannel } from '../structures/guildTextChannel.ts' +import { GuildTextChannel } from '../structures/textChannel.ts' import { VoiceChannel } from '../structures/guildVoiceChannel.ts' import { GuildChannelCategoryPayload, diff --git a/src/managers/memberRoles.ts b/src/managers/memberRoles.ts new file mode 100644 index 0000000..a287fa4 --- /dev/null +++ b/src/managers/memberRoles.ts @@ -0,0 +1,42 @@ +import { Client } from '../models/client.ts' +import { BaseChildManager } from './baseChild.ts' +import { RolePayload } from "../types/role.ts" +import { Role } from "../structures/role.ts" +import { Member } from "../structures/member.ts" +import { RolesManager } from "./roles.ts" +import { MemberPayload } from "../types/guild.ts" + +export class MemberRolesManager extends BaseChildManager< + RolePayload, + Role +> { + member: Member + + constructor (client: Client, parent: RolesManager, member: Member) { + super(client, parent as any) + this.member = member + } + + async get (id: string): Promise { + const res = await this.parent.get(id) + const mem = await (this.parent as any).guild.members._get(this.member.id) as MemberPayload + if (res !== undefined && mem.roles.includes(res.id) === true) return res + else return undefined + } + + async array (): Promise { + const arr = (await this.parent.array()) as Role[] + const mem = await (this.parent as any).guild.members._get(this.member.id) as MemberPayload + return arr.filter( + (c: any) => mem.roles.includes(c.id) + ) as any + } + + async flush (): Promise { + const arr = await this.array() + for (const elem of arr) { + this.parent.delete(elem.id) + } + return true + } +} diff --git a/src/managers/members.ts b/src/managers/members.ts index acf6e2c..45385fa 100644 --- a/src/managers/members.ts +++ b/src/managers/members.ts @@ -18,11 +18,7 @@ export class MembersManager extends BaseManager { const raw = await this._get(key) if (raw === undefined) return const user = new User(this.client, raw.user) - const res = new this.DataType(this.client, raw, user) - for (const roleid of res.roleIDs as string[]) { - const role = await this.guild.roles.get(roleid) - if (role !== undefined) res.roles.push(role) - } + const res = new this.DataType(this.client, raw, user, this.guild) return res } @@ -31,11 +27,7 @@ export class MembersManager extends BaseManager { this.client.rest.get(GUILD_MEMBER(this.guild.id, id)).then(async data => { await this.set(id, data as MemberPayload) const user: User = new User(this.client, data.user) - const res = new Member(this.client, data as MemberPayload, user) - for (const roleid of res.roleIDs as string[]) { - const role = await this.guild.roles.get(roleid) - if (role !== undefined) res.roles.push(role) - } + const res = new Member(this.client, data as MemberPayload, user, this.guild) resolve(res) }).catch(e => reject(e)) }) diff --git a/src/managers/messages.ts b/src/managers/messages.ts index 1cfeaff..f1cddbb 100644 --- a/src/managers/messages.ts +++ b/src/managers/messages.ts @@ -1,6 +1,5 @@ import { Client } from '../models/client.ts' import { Message } from '../structures/message.ts' -import { MessageMentions } from '../structures/messageMentions.ts' import { TextChannel } from '../structures/textChannel.ts' import { User } from '../structures/user.ts' import { MessagePayload } from '../types/channel.ts' @@ -21,9 +20,10 @@ export class MessagesManager extends BaseManager { channel = await this.client.channels.fetch(raw.channel_id) const author = new User(this.client, raw.author) - const mentions = new MessageMentions() - return new this.DataType(this.client, raw, channel, author, mentions) as any + const res = new this.DataType(this.client, raw, channel, author) as any + await res.mentions.fromPayload(raw) + return res } async fetch (channelID: string, id: string): Promise { @@ -45,18 +45,16 @@ export class MessagesManager extends BaseManager { (data as MessagePayload).author ) - // TODO: Make this thing work (MessageMentions) - const mentions = new MessageMentions() - - resolve( - new Message( - this.client, - data as MessagePayload, - channel as TextChannel, - author, - mentions - ) + const res = new Message( + this.client, + data as MessagePayload, + channel as TextChannel, + author ) + + await res.mentions.fromPayload(data) + + resolve(res) }) .catch(e => reject(e)) }) diff --git a/src/models/command.ts b/src/models/command.ts index 1f48a75..277face 100644 --- a/src/models/command.ts +++ b/src/models/command.ts @@ -35,6 +35,8 @@ export class Command { description?: string /** Array of Aliases of Command, or only string */ aliases?: string | string[] + /** Category of the Command */ + category?: string /** Usage of Command, only Argument Names */ usage?: string | string[] /** Usage Example of Command, only Arguments (without Prefix and Name) */ @@ -50,19 +52,29 @@ export class Command { /** Whether the Command can only be used by Bot Owners */ ownerOnly?: boolean - execute (ctx?: CommandContext): any {} + /** Method executed before executing actual command. Returns bool value - whether to continue or not (optional) */ + beforeExecute(ctx: CommandContext): boolean | Promise { return true } + /** Actual command code, which is executed when all checks have passed. */ + execute(ctx: CommandContext): any { } + /** Method executed after executing command, passes on CommandContext and the value returned by execute too. (optional) */ + afterExecute(ctx: CommandContext, executeResult: any): any { } } export class CommandsManager { client: CommandClient list: Collection = new Collection() + disabled: Set = new Set() + disabledCategories: Set = new Set() - constructor (client: CommandClient) { + constructor(client: CommandClient) { this.client = client } + /** Number of loaded Commands */ + get count(): number { return this.list.size } + /** Find a Command by name/alias */ - find (search: string): Command | undefined { + find(search: string): Command | undefined { if (this.client.caseSensitive === false) search = search.toLowerCase() return this.list.find((cmd: Command): boolean => { const name = @@ -79,8 +91,17 @@ export class CommandsManager { }) } + /** Fetch a Command including disable checks */ + fetch(name: string, bypassDisable?: boolean): Command | undefined { + const cmd = this.find(name) + if (cmd === undefined) return + if (this.isDisabled(cmd) && bypassDisable !== true) return + if (cmd.category !== undefined && this.isCategoryDisabled(cmd.category) && bypassDisable !== true) return + return cmd + } + /** Check whether a Command exists or not */ - exists (search: Command | string): boolean { + exists(search: Command | string): boolean { let exists = false if (typeof search === 'string') return this.find(search) !== undefined else { @@ -97,7 +118,7 @@ export class CommandsManager { } /** Add a Command */ - add (cmd: Command | typeof Command): boolean { + add(cmd: Command | typeof Command): boolean { // eslint-disable-next-line new-cap if (!(cmd instanceof Command)) cmd = new cmd() if (this.exists(cmd)) return false @@ -106,11 +127,46 @@ export class CommandsManager { } /** Delete a Command */ - delete (cmd: string | Command): boolean { + delete(cmd: string | Command): boolean { const find = typeof cmd === 'string' ? this.find(cmd) : cmd if (find === undefined) return false else return this.list.delete(find.name) } + + /** Get all Commands of given Category */ + category(name: string): Collection { + return this.list.filter(c => c.category === name) + } + + /** Check whether a Command is disabled or not */ + isDisabled(name: string | Command): boolean { + const cmd = typeof name === "string" ? this.find(name) : name + if (cmd === undefined) return false + const exists = this.exists(name) + if (!exists) return false + return this.disabled.has(cmd.name) + } + + /** Disable a Command */ + disable(name: string | Command): boolean { + const cmd = typeof name === "string" ? this.find(name) : name + if (cmd === undefined) return false + if (this.isDisabled(cmd)) return false + this.disabled.add(cmd.name) + return true + } + + /** Check whether a Category is disabled */ + isCategoryDisabled(name: string): boolean { + return this.disabledCategories.has(name) + } + + /** Disable a Category of Commands */ + disableCategory(name: string): boolean { + if (this.isCategoryDisabled(name)) return false + this.disabledCategories.add(name) + return true + } } export interface ParsedCommand { diff --git a/src/models/commandClient.ts b/src/models/commandClient.ts index 07ba382..3de7ced 100644 --- a/src/models/commandClient.ts +++ b/src/models/commandClient.ts @@ -1,4 +1,5 @@ import { Embed, Message } from '../../mod.ts' +import { awaitSync } from "../utils/mixedPromise.ts" import { Client, ClientOptions } from './client.ts' import { CommandContext, CommandsManager, parseCommand } from './command.ts' @@ -9,6 +10,9 @@ export interface CommandClientOptions extends ClientOptions { mentionPrefix?: boolean getGuildPrefix?: (guildID: string) => PrefixReturnType getUserPrefix?: (userID: string) => PrefixReturnType + isGuildBlacklisted?: (guildID: string) => boolean | Promise + isUserBlacklisted?: (guildID: string) => boolean | Promise + isChannelBlacklisted?: (guildID: string) => boolean | Promise spacesAfterPrefix?: boolean betterArgs?: boolean owners?: string[] @@ -49,6 +53,9 @@ export class CommandClient extends Client implements CommandClientOptions { mentionPrefix: boolean getGuildPrefix: (guildID: string) => PrefixReturnType getUserPrefix: (userID: string) => PrefixReturnType + isGuildBlacklisted: (guildID: string) => boolean | Promise + isUserBlacklisted: (guildID: string) => boolean | Promise + isChannelBlacklisted: (guildID: string) => boolean | Promise spacesAfterPrefix: boolean betterArgs: boolean owners: string[] @@ -58,7 +65,7 @@ export class CommandClient extends Client implements CommandClientOptions { commands: CommandsManager = new CommandsManager(this) texts: CommandTexts = DefaultCommandTexts - constructor (options: CommandClientOptions) { + constructor(options: CommandClientOptions) { super(options) this.prefix = options.prefix this.mentionPrefix = @@ -71,6 +78,18 @@ export class CommandClient extends Client implements CommandClientOptions { options.getUserPrefix === undefined ? (id: string) => this.prefix : options.getUserPrefix + this.isUserBlacklisted = + options.isUserBlacklisted === undefined + ? (id: string) => false + : options.isUserBlacklisted + this.isGuildBlacklisted = + options.isGuildBlacklisted === undefined + ? (id: string) => false + : options.isGuildBlacklisted + this.isChannelBlacklisted = + options.isChannelBlacklisted === undefined + ? (id: string) => false + : options.isChannelBlacklisted this.spacesAfterPrefix = options.spacesAfterPrefix === undefined ? false @@ -89,29 +108,52 @@ export class CommandClient extends Client implements CommandClientOptions { ) } - async processMessage (msg: Message): Promise { + async processMessage(msg: Message): Promise { if (!this.allowBots && msg.author.bot === true) return + const isUserBlacklisted = await awaitSync(this.isUserBlacklisted(msg.author.id)) + if (isUserBlacklisted === true) return + + const isChannelBlacklisted = await awaitSync(this.isChannelBlacklisted(msg.channel.id)) + if (isChannelBlacklisted === true) return + + if (msg.guild !== undefined) { + const isGuildBlacklisted = await awaitSync(this.isGuildBlacklisted(msg.guild.id)) + if (isGuildBlacklisted === true) return + } + let prefix: string | string[] = this.prefix if (msg.guild !== undefined) { - let guildPrefix = this.getGuildPrefix(msg.guild.id) - if (guildPrefix instanceof Promise) guildPrefix = await guildPrefix - prefix = guildPrefix + prefix = await awaitSync(this.getGuildPrefix(msg.guild.id)) } else { - let userPrefix = this.getUserPrefix(msg.author.id) - if (userPrefix instanceof Promise) userPrefix = await userPrefix - prefix = userPrefix + prefix = await awaitSync(this.getUserPrefix(msg.author.id)) } + let mentionPrefix = false + if (typeof prefix === 'string') { - if (msg.content.startsWith(prefix) === false) return + if (msg.content.startsWith(prefix) === false) { + if (this.mentionPrefix) mentionPrefix = true + else return + } } else { const usedPrefix = prefix.find(v => msg.content.startsWith(v)) - if (usedPrefix === undefined) return + if (usedPrefix === undefined) { + if (this.mentionPrefix) mentionPrefix = true + else return + } else prefix = usedPrefix } + if (mentionPrefix) { + if (msg.content.startsWith(this.user?.mention as string) === true) prefix = this.user?.mention as string + else if (msg.content.startsWith(this.user?.nickMention as string) === true) prefix = this.user?.nickMention as string + else return + } + + if (typeof prefix !== 'string') return + const parsed = parseCommand(this, msg, prefix) const command = this.commands.find(parsed.name) @@ -158,10 +200,15 @@ export class CommandClient extends Client implements CommandClientOptions { try { this.emit('commandUsed', { context: ctx }) - command.execute(ctx) + + const beforeExecute = await awaitSync(command.beforeExecute(ctx)) + if (beforeExecute === false) return + + const result = await awaitSync(command.execute(ctx)) + command.afterExecute(ctx, result) } catch (e) { if (this.texts.ERROR !== undefined) - return this.sendProcessedText( + this.sendProcessedText( msg, this.texts.ERROR, Object.assign(baseReplaces, { error: e.message }) @@ -170,7 +217,7 @@ export class CommandClient extends Client implements CommandClientOptions { } } - sendProcessedText (msg: Message, text: CommandText, replaces: Replaces): any { + sendProcessedText(msg: Message, text: CommandText, replaces: Replaces): any { if (typeof text === 'string') { text = massReplace(text, replaces) return msg.channel.send(text) diff --git a/src/models/rest.ts b/src/models/rest.ts index 24e468f..603b8ce 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -25,13 +25,13 @@ export interface RequestHeaders { } export interface QueuedItem { + bucket?: string | null + url: string onComplete: () => Promise<{ rateLimited: any bucket?: string | null before: boolean } | undefined> - bucket?: string | null - url: string } export interface RateLimit { @@ -93,10 +93,8 @@ export class RESTManager { request.bucket ) if (rateLimitResetIn !== false) { - // This request is still rate limited read to queue this.queue(request) } else { - // This request is not rate limited so it should be run const result = await request.onComplete() if (result?.rateLimited !== undefined) { this.queue({ @@ -107,10 +105,8 @@ export class RESTManager { } } else { if (rateLimitedURLResetIn !== false) { - // This URL is rate limited readd to queue this.queue(request) } else { - // This request has no bucket id so it should be processed const result = await request.onComplete() if (result?.rateLimited !== undefined) { this.queue({ @@ -253,27 +249,35 @@ export class RESTManager { } async handleStatusCode( - response: Response + response: Response, body: any, data: { [key: string]: any } ): Promise { const status = response.status - if ((status >= 200 && status < 400) || status === HttpResponseCode.TooManyRequests) return + if ( + (status >= 200 && status < 400) + || status === HttpResponseCode.NoContent + || status === HttpResponseCode.TooManyRequests + ) return - const body = await response.json() - const text = Deno.inspect(body.errors) + let text: undefined | string = Deno.inspect(body.errors === undefined ? body : body.errors) + if (text === 'undefined') text = undefined if (status === HttpResponseCode.Unauthorized) throw new Error(`Request was not successful (Unauthorized). Invalid Token.\n${text}`) + // At this point we know it is error + let error = { url: response.url, status, method: data.method, body: data.body } + if (body !== undefined) error = Object.assign(error, body) + if ([ HttpResponseCode.BadRequest, HttpResponseCode.NotFound, HttpResponseCode.Forbidden, HttpResponseCode.MethodNotAllowed ].includes(status)) { - throw new Error(`Request - Client Error. Code: ${status}\n${text}`) + throw new Error(Deno.inspect(error)) } else if (status === HttpResponseCode.GatewayUnavailable) { - throw new Error(`Request - Server Error. Code: ${status}\n${text}`) + throw new Error(Deno.inspect(error)) } else throw new Error('Request - Unknown Error') } @@ -319,12 +323,13 @@ export class RESTManager { const response = await fetch(urlToUse, requestData) const bucketFromHeaders = this.processHeaders(url, response.headers) - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.handleStatusCode(response) if (response.status === 204) return resolve(undefined) - const json = await response.json() + const json: any = await response.json() + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.handleStatusCode(response, json, requestData) + if ( json.retry_after !== undefined || json.message === 'You are being rate limited.' diff --git a/src/structures/guildTextChannel.ts b/src/structures/guildTextChannel.ts deleted file mode 100644 index 385f220..0000000 --- a/src/structures/guildTextChannel.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Client } from '../models/client.ts' -import { GuildTextChannelPayload, Overwrite } from '../types/channel.ts' -import { TextChannel } from './textChannel.ts' -import { Guild } from './guild.ts' - -export class GuildTextChannel extends TextChannel { - guildID: string - name: string - position: number - permissionOverwrites: Overwrite[] - nsfw: boolean - parentID?: string - rateLimit: number - topic?: string - guild: Guild - - get mention (): string { - return `<#${this.id}>` - } - - constructor (client: Client, data: GuildTextChannelPayload, guild: Guild) { - super(client, data) - this.guildID = data.guild_id - this.name = data.name - this.guild = guild - this.position = data.position - this.permissionOverwrites = data.permission_overwrites - this.nsfw = data.nsfw - this.parentID = data.parent_id - this.topic = data.topic - this.rateLimit = data.rate_limit_per_user - // TODO: Cache in Gateway Event Code - // cache.set('guildtextchannel', this.id, this) - } - - protected readFromData (data: GuildTextChannelPayload): void { - super.readFromData(data) - this.guildID = data.guild_id ?? this.guildID - this.name = data.name ?? this.name - this.position = data.position ?? this.position - this.permissionOverwrites = - data.permission_overwrites ?? this.permissionOverwrites - this.nsfw = data.nsfw ?? this.nsfw - this.parentID = data.parent_id ?? this.parentID - this.topic = data.topic ?? this.topic - this.rateLimit = data.rate_limit_per_user ?? this.rateLimit - } -} diff --git a/src/structures/member.ts b/src/structures/member.ts index d970221..dc6d03e 100644 --- a/src/structures/member.ts +++ b/src/structures/member.ts @@ -1,28 +1,30 @@ +import { MemberRolesManager } from "../managers/memberRoles.ts" import { Client } from '../models/client.ts' import { MemberPayload } from '../types/guild.ts' import { Base } from './base.ts' -import { Role } from './role.ts' +import { Guild } from "./guild.ts" import { User } from './user.ts' export class Member extends Base { id: string user: User nick?: string - roleIDs: string[] - roles: Role[] = [] + roles: MemberRolesManager joinedAt: string premiumSince?: string deaf: boolean mute: boolean + guild: Guild - constructor (client: Client, data: MemberPayload, user: User) { + constructor (client: Client, data: MemberPayload, user: User, guild: Guild) { super(client) this.id = data.user.id this.user = user // this.user = // cache.get('user', data.user.id) ?? new User(this.client, data.user) this.nick = data.nick - this.roleIDs = data.roles + this.guild = guild + this.roles = new MemberRolesManager(this.client, this.guild.roles, this) this.joinedAt = data.joined_at this.premiumSince = data.premium_since this.deaf = data.deaf @@ -34,7 +36,6 @@ export class Member extends Base { protected readFromData (data: MemberPayload): void { super.readFromData(data.user) this.nick = data.nick ?? this.nick - this.roleIDs = data.roles ?? this.roles.map(r => r.id) this.joinedAt = data.joined_at ?? this.joinedAt this.premiumSince = data.premium_since ?? this.premiumSince this.deaf = data.deaf ?? this.deaf diff --git a/src/structures/message.ts b/src/structures/message.ts index c1b29ab..0eaaa43 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -19,9 +19,9 @@ import { TextChannel } from './textChannel.ts' import { DMChannel } from './dmChannel.ts' import { Guild } from './guild.ts' +type AllMessageOptions = MessageOption | Embed + export class Message extends Base { - // eslint-disable-next-line @typescript-eslint/prefer-readonly - private data: MessagePayload id: string channelID: string channel: TextChannel @@ -53,26 +53,19 @@ export class Message extends Base { client: Client, data: MessagePayload, channel: TextChannel, - author: User, - mentions: MessageMentions + author: User ) { super(client) - this.data = data this.id = data.id this.channelID = data.channel_id this.guildID = data.guild_id this.author = author - // this.author = - // this.client.users.get(data.author.id) || new User(this.client, data.author) this.content = data.content this.timestamp = data.timestamp this.editedTimestamp = data.edited_timestamp this.tts = data.tts this.mentionEveryone = data.mention_everyone - this.mentions = mentions - // this.mentions = data.mentions.map( - // v => this.client.users.get(v.id) || new User(client, v) - // ) + this.mentions = new MessageMentions(this.client, this) this.mentionRoles = data.mention_roles this.mentionChannels = data.mention_channels this.attachments = data.attachments @@ -87,27 +80,17 @@ export class Message extends Base { this.messageReference = data.message_reference this.flags = data.flags this.channel = channel - // TODO: Cache in Gateway Event Code - // if (!noSave) this.client.messages.set(this.id, data) } protected readFromData (data: MessagePayload): void { super.readFromData(data) this.channelID = data.channel_id ?? this.channelID this.guildID = data.guild_id ?? this.guildID - // this.author = - // this.client.users.get(data.author.id) || - // this.author || - // new User(this.client, data.author) this.content = data.content ?? this.content this.timestamp = data.timestamp ?? this.timestamp this.editedTimestamp = data.edited_timestamp ?? this.editedTimestamp this.tts = data.tts ?? this.tts this.mentionEveryone = data.mention_everyone ?? this.mentionEveryone - // this.mentions = - // data.mentions.map( - // v => this.client.users.get(v.id) || new User(this.client, v) - // ) ?? this.mentions this.mentionRoles = data.mention_roles ?? this.mentionRoles this.mentionChannels = data.mention_channels ?? this.mentionChannels this.attachments = data.attachments ?? this.attachments @@ -124,13 +107,13 @@ export class Message extends Base { } async edit (text?: string, option?: MessageOption): Promise { - return this.channel.edit(this.id, text, option) + return this.channel.editMessage(this.id, text, option) } - async reply(text: string, options?: MessageOption): Promise { + async reply(text?: string | AllMessageOptions, option?: AllMessageOptions): Promise { // TODO: Use inline replies once they're out - if (this.channel instanceof DMChannel) return this.channel.send(text, options) - return this.channel.send(`${this.author.mention}, ${text}`, options) + if (this.channel instanceof DMChannel) return this.channel.send(text, option) + return this.channel.send(`${this.author.mention}, ${text}`, option) } async delete (): Promise { diff --git a/src/structures/messageMentions.ts b/src/structures/messageMentions.ts index 6ef2a00..bf2e252 100644 --- a/src/structures/messageMentions.ts +++ b/src/structures/messageMentions.ts @@ -1,3 +1,55 @@ +import { Client } from "../models/client.ts"; +import { MessagePayload } from "../types/channel.ts"; +import { Collection } from "../utils/collection.ts"; +import { GuildTextChannel } from "./textChannel.ts"; +import { Message } from "./message.ts"; +import { Role } from "./role.ts"; +import { User } from "./user.ts"; + export class MessageMentions { - str: string = "str" + client: Client + message: Message + users: Collection = new Collection() + roles: Collection = new Collection() + channels: Collection = new Collection() + everyone: boolean = false + + static EVERYONE_MENTION = /@(everyone|here)/g + static USER_MENTION = /<@!?(\d{17,19})>/g + static ROLE_MENTION = /<@&(\d{17,19})>/g + static CHANNEL_MENTION = /<#(\d{17,19})>/g + + constructor(client: Client, message: Message) { + this.client = client + this.message = message + } + + async fromPayload(payload: MessagePayload): Promise { + payload.mentions.forEach(rawUser => { + this.users.set(rawUser.id, new User(this.client, rawUser)) + }) + + if (this.message.guild !== undefined) { + for (const id of payload.mention_roles) { + const role = await this.message.guild.roles.get(id) + if(role !== undefined) this.roles.set(role.id, role) + } + } + if (payload.mention_channels !== undefined) { + for (const mentionChannel of payload.mention_channels) { + const channel = await this.client.channels.get(mentionChannel.id) + if (channel !== undefined) this.channels.set(channel.id, channel) + } + } + const matchChannels = this.message.content.match(MessageMentions.CHANNEL_MENTION) + if (matchChannels !== null) { + for (const id of matchChannels) { + const parsedID = id.substr(2, id.length - 3) + const channel = await this.client.channels.get(parsedID) + if (channel !== undefined) this.channels.set(channel.id, channel) + } + } + this.everyone = payload.mention_everyone + return this + } } diff --git a/src/structures/role.ts b/src/structures/role.ts index c955e50..7c8b328 100644 --- a/src/structures/role.ts +++ b/src/structures/role.ts @@ -16,6 +16,8 @@ export class Role extends Base { return `<@&${this.id}>` } + toString(): string { return this.mention } + constructor (client: Client, data: RolePayload) { super(client, data) this.id = data.id diff --git a/src/structures/textChannel.ts b/src/structures/textChannel.ts index d4e815c..4962b99 100644 --- a/src/structures/textChannel.ts +++ b/src/structures/textChannel.ts @@ -1,10 +1,10 @@ import { Client } from '../models/client.ts' -import { MessageOption, TextChannelPayload } from '../types/channel.ts' +import { GuildTextChannelPayload, MessageOption, Overwrite, TextChannelPayload } from '../types/channel.ts' import { CHANNEL_MESSAGE, CHANNEL_MESSAGES } from '../types/endpoint.ts' import { Channel } from './channel.ts' import { Embed } from './embed.ts' +import { Guild } from "./guild.ts" import { Message } from './message.ts' -import { MessageMentions } from './messageMentions.ts' type AllMessageOptions = MessageOption | Embed @@ -16,8 +16,6 @@ export class TextChannel extends Channel { super(client, data) this.lastMessageID = data.last_message_id this.lastPinTimestamp = data.last_pin_timestamp - // TODO: Cache in Gateway Event Code - // cache.set('textchannel', this.id, this) } protected readFromData (data: TextChannelPayload): void { @@ -46,10 +44,12 @@ export class TextChannel extends Channel { allowed_mentions: option?.allowedMention }) - return new Message(this.client, resp as any, this, this.client.user as any, new MessageMentions()) + const res = new Message(this.client, resp, this, this.client.user as any) + await res.mentions.fromPayload(resp) + return res } - async edit ( + async editMessage ( message: Message | string, text?: string, option?: MessageOption @@ -76,9 +76,54 @@ export class TextChannel extends Channel { } ) - // TODO: Actually construct this object - const mentions = new MessageMentions() - - return new Message(this.client, newMsg, this, this.client.user, mentions) + const res = new Message(this.client, newMsg, this, this.client.user) + await res.mentions.fromPayload(newMsg) + return res + } +} + +export class GuildTextChannel extends TextChannel { + guildID: string + name: string + position: number + permissionOverwrites: Overwrite[] + nsfw: boolean + parentID?: string + rateLimit: number + topic?: string + guild: Guild + + get mention (): string { + return `<#${this.id}>` + } + + toString(): string { + return this.mention + } + + constructor (client: Client, data: GuildTextChannelPayload, guild: Guild) { + super(client, data) + this.guildID = data.guild_id + this.name = data.name + this.guild = guild + this.position = data.position + this.permissionOverwrites = data.permission_overwrites + this.nsfw = data.nsfw + this.parentID = data.parent_id + this.topic = data.topic + this.rateLimit = data.rate_limit_per_user + } + + protected readFromData (data: GuildTextChannelPayload): void { + super.readFromData(data) + this.guildID = data.guild_id ?? this.guildID + this.name = data.name ?? this.name + this.position = data.position ?? this.position + this.permissionOverwrites = + data.permission_overwrites ?? this.permissionOverwrites + this.nsfw = data.nsfw ?? this.nsfw + this.parentID = data.parent_id ?? this.parentID + this.topic = data.topic ?? this.topic + this.rateLimit = data.rate_limit_per_user ?? this.rateLimit } } diff --git a/src/test/cmd.ts b/src/test/cmd.ts index 8d4a948..f4d7fe0 100644 --- a/src/test/cmd.ts +++ b/src/test/cmd.ts @@ -1,12 +1,10 @@ import { CommandClient, Intents } from '../../mod.ts' -import PingCommand from './cmds/ping.ts' -import AddEmojiCommand from './cmds/addemoji.ts' -import UserinfoCommand from './cmds/userinfo.ts' import { TOKEN } from './config.ts' const client = new CommandClient({ prefix: ["pls", "!"], - spacesAfterPrefix: true + spacesAfterPrefix: true, + mentionPrefix: true }) client.on('debug', console.log) @@ -15,10 +13,23 @@ client.on('ready', () => { console.log(`[Login] Logged in as ${client.user?.tag}!`) }) +// client.on('messageCreate', msg => console.log(`${msg.author.tag}: ${msg.content}`)) + client.on("commandError", console.error) -client.commands.add(PingCommand) -client.commands.add(UserinfoCommand) -client.commands.add(AddEmojiCommand) +// eslint-disable-next-line @typescript-eslint/no-floating-promises +;(async() => { + const files = Deno.readDirSync('./src/test/cmds') -client.connect(TOKEN, Intents.All) \ No newline at end of file + for (const file of files) { + const module = await import(`./cmds/${file.name}`) + // eslint-disable-next-line new-cap + const cmd = new module.default() + client.commands.add(cmd) + console.log(`Loaded command ${cmd.name}!`) + } + + console.log(`Loaded ${client.commands.count} commands!`) + + client.connect(TOKEN, Intents.All) +})() \ No newline at end of file diff --git a/src/test/cmds/mentions.ts b/src/test/cmds/mentions.ts new file mode 100644 index 0000000..63984d5 --- /dev/null +++ b/src/test/cmds/mentions.ts @@ -0,0 +1,18 @@ +import { Command, Embed } from '../../../mod.ts' +import { CommandContext } from '../../models/command.ts' + +export default class PingCommand extends Command { + name = "mentions" + aliases = ["m"] + + execute(ctx: CommandContext): void { + const embed = new Embed() + .setTitle('Mentions') + .addField('Users', `${ctx.message.mentions.users.size === 0 ? `None` : ''}${ctx.message.mentions.users.map(u => u.toString()).join(", ")}`) + .addField('Channels', `${ctx.message.mentions.channels.size === 0 ? `None` : ''}${ctx.message.mentions.channels.map(u => u.toString()).join(", ")}`) + .addField('Roles', `${ctx.message.mentions.roles.size === 0 ? `None` : ''}${ctx.message.mentions.roles.map(u => u.toString()).join(", ")}`) + .addField('Everyone?', ctx.message.mentions.everyone === true ? 'Yes' : 'No') + .setColor(0xff0000) + ctx.message.channel.send(embed) + } +} \ No newline at end of file diff --git a/src/test/cmds/userinfo.ts b/src/test/cmds/userinfo.ts index 9b65e2f..efbb062 100644 --- a/src/test/cmds/userinfo.ts +++ b/src/test/cmds/userinfo.ts @@ -3,14 +3,17 @@ import { Command, Member, CommandContext, Embed } from '../../../mod.ts' export default class UserinfoCommand extends Command { name = "userinfo" guildOnly = true + aliases = [ 'u', 'user' ] - execute(ctx: CommandContext): void { + async execute(ctx: CommandContext): Promise { const member: Member = ctx.message.member as any + const roles = await member.roles.array() const embed = new Embed() .setTitle(`User Info`) .setAuthor({ name: member.user.tag }) .addField("ID", member.id) - .addField("Roles", member.roles.map(r => r.name).join(", ")) + .addField("Roles", roles.map(r => r.name).join(", ")) + .setColor(0xff00ff) ctx.channel.send(embed) } } \ No newline at end of file diff --git a/src/utils/getChannelByType.ts b/src/utils/getChannelByType.ts index b15d38b..5dbb82b 100644 --- a/src/utils/getChannelByType.ts +++ b/src/utils/getChannelByType.ts @@ -15,8 +15,7 @@ import { CategoryChannel } from '../structures/guildCategoryChannel.ts' import { NewsChannel } from '../structures/guildNewsChannel.ts' import { VoiceChannel } from '../structures/guildVoiceChannel.ts' import { Guild } from '../structures/guild.ts' -import { GuildTextChannel } from '../structures/guildTextChannel.ts' -import { TextChannel } from '../structures/textChannel.ts' +import { TextChannel, GuildTextChannel } from '../structures/textChannel.ts' const getChannelByType = ( client: Client, diff --git a/src/utils/mixedPromise.ts b/src/utils/mixedPromise.ts new file mode 100644 index 0000000..749937f --- /dev/null +++ b/src/utils/mixedPromise.ts @@ -0,0 +1,3 @@ +export const awaitSync = async(val: any | Promise): Promise => { + return val instanceof Promise ? await val : val +} \ No newline at end of file