diff --git a/src/managers/base.ts b/src/managers/base.ts index 442b7fc..7efa4e0 100644 --- a/src/managers/base.ts +++ b/src/managers/base.ts @@ -64,7 +64,21 @@ export class BaseManager { const arr = (await this.array()) ?? [] const { readable, writable } = new TransformStream() arr.forEach((el) => writable.getWriter().write(el)) - yield* readable.getIterator() + yield* readable + } + + async fetch(...args: unknown[]): Promise { + return undefined + } + + /** Try to get value from cache, if not found then fetch */ + async resolve(key: string): Promise { + const cacheValue = await this.get(key) + if (cacheValue !== undefined) return cacheValue + else { + const fetchValue = await this.fetch(key).catch(() => undefined) + if (fetchValue !== undefined) return fetchValue + } } /** Deletes everything from Cache */ diff --git a/src/managers/baseChild.ts b/src/managers/baseChild.ts index 0842859..446a5d3 100644 --- a/src/managers/baseChild.ts +++ b/src/managers/baseChild.ts @@ -44,6 +44,20 @@ export class BaseChildManager { const arr = (await this.array()) ?? [] const { readable, writable } = new TransformStream() arr.forEach((el: unknown) => writable.getWriter().write(el)) - yield* readable.getIterator() + yield* readable + } + + async fetch(...args: unknown[]): Promise { + return this.parent.fetch(...args) + } + + /** Try to get value from cache, if not found then fetch */ + async resolve(key: string): Promise { + const cacheValue = await this.get(key) + if (cacheValue !== undefined) return cacheValue + else { + const fetchValue = await this.fetch(key).catch(() => undefined) + if (fetchValue !== undefined) return fetchValue + } } } diff --git a/src/managers/channels.ts b/src/managers/channels.ts index f7bdae4..4980802 100644 --- a/src/managers/channels.ts +++ b/src/managers/channels.ts @@ -1,10 +1,19 @@ import { Client } from '../models/client.ts' import { Channel } from '../structures/channel.ts' -import { ChannelPayload, GuildChannelPayload } from '../types/channel.ts' +import { Embed } from '../structures/embed.ts' +import { Message } from '../structures/message.ts' +import { TextChannel } from '../structures/textChannel.ts' +import { + ChannelPayload, + GuildChannelPayload, + MessageOptions +} from '../types/channel.ts' import { CHANNEL } from '../types/endpoint.ts' import getChannelByType from '../utils/getChannelByType.ts' import { BaseManager } from './base.ts' +export type AllMessageOptions = MessageOptions | Embed + export class ChannelsManager extends BaseManager { constructor(client: Client) { super(client, 'channels', Channel) @@ -66,4 +75,105 @@ export class ChannelsManager extends BaseManager { .catch((e) => reject(e)) }) } + + async sendMessage( + channel: string | TextChannel, + content?: string | AllMessageOptions, + option?: AllMessageOptions + ): Promise { + const channelID = typeof channel === 'string' ? channel : channel.id + + if (typeof content === 'object') { + option = content + content = undefined + } + if (content === undefined && option === undefined) { + throw new Error('Either text or option is necessary.') + } + if (option instanceof Embed) { + option = { + embed: option + } + } + + const payload: any = { + content: content, + embed: option?.embed, + file: option?.file, + files: option?.files, + tts: option?.tts, + allowed_mentions: option?.allowedMentions, + message_reference: + option?.reply === undefined + ? undefined + : typeof option.reply === 'string' + ? { + message_id: option.reply + } + : typeof option.reply === 'object' + ? option.reply instanceof Message + ? { + message_id: option.reply.id, + channel_id: option.reply.channel.id, + guild_id: option.reply.guild?.id + } + : option.reply + : undefined + } + + const resp = await this.client.rest.api.channels[channelID].messages.post( + payload + ) + const chan = + typeof channel === 'string' + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (await this.get(channel))! + : channel + const res = new Message(this.client, resp, chan, this.client.user as any) + await res.mentions.fromPayload(resp) + return res + } + + async editMessage( + channel: string | TextChannel, + message: Message | string, + text?: string | MessageOptions, + option?: MessageOptions + ): Promise { + const channelID = typeof channel === 'string' ? channel : channel.id + + if (text === undefined && option === undefined) { + throw new Error('Either text or option is necessary.') + } + + if (this.client.user === undefined) { + throw new Error('Client user has not initialized.') + } + + if (typeof text === 'object') { + if (typeof option === 'object') Object.assign(option, text) + else option = text + text = undefined + } + + const newMsg = await this.client.rest.api.channels[channelID].messages[ + typeof message === 'string' ? message : message.id + ].patch({ + content: text, + embed: option?.embed !== undefined ? option.embed.toJSON() : undefined, + // Cannot upload new files with Message + // file: option?.file, + tts: option?.tts, + allowed_mentions: option?.allowedMentions + }) + + const chan = + typeof channel === 'string' + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (await this.get(channel))! + : channel + const res = new Message(this.client, newMsg, chan, this.client.user) + await res.mentions.fromPayload(newMsg) + return res + } } diff --git a/src/models/command.ts b/src/models/command.ts index 05e59ff..4040449 100644 --- a/src/models/command.ts +++ b/src/models/command.ts @@ -92,6 +92,9 @@ export class Command implements CommandOptions { dmOnly?: boolean ownerOnly?: boolean + /** Method called when the command errors */ + onError(ctx: CommandContext, error: Error): any {} + /** Method executed before executing actual command. Returns bool value - whether to continue or not (optional) */ beforeExecute(ctx: CommandContext): boolean | Promise { return true diff --git a/src/models/commandClient.ts b/src/models/commandClient.ts index 67d121d..1c78a13 100644 --- a/src/models/commandClient.ts +++ b/src/models/commandClient.ts @@ -286,7 +286,8 @@ export class CommandClient extends Client implements CommandClientOptions { if ( (command.botPermissions !== undefined || - category?.permissions !== undefined) && + category?.botPermissions !== undefined || + allPermissions !== undefined) && msg.guild !== undefined ) { // TODO: Check Overwrites too @@ -315,7 +316,8 @@ export class CommandClient extends Client implements CommandClientOptions { if ( (command.userPermissions !== undefined || - category?.userPermissions !== undefined) && + category?.userPermissions !== undefined || + allPermissions !== undefined) && msg.guild !== undefined ) { let permissions = @@ -358,8 +360,11 @@ export class CommandClient extends Client implements CommandClientOptions { if (beforeExecute === false) return const result = await command.execute(ctx) - command.afterExecute(ctx, result) + await command.afterExecute(ctx, result) } catch (e) { + await command + .onError(ctx, e) + .catch((e: Error) => this.emit('commandError', ctx, e)) this.emit('commandError', ctx, e) } } @@ -375,7 +380,7 @@ export function command(options?: CommandOptions) { })[name] if (typeof prop !== 'function') - throw new Error('@command decorator can only be used on functions') + throw new Error('@command decorator can only be used on class methods') const command = new Command() diff --git a/src/structures/textChannel.ts b/src/structures/textChannel.ts index 5f05f8d..0520520 100644 --- a/src/structures/textChannel.ts +++ b/src/structures/textChannel.ts @@ -5,7 +5,6 @@ import { GuildTextChannelPayload, MessageOptions, MessagePayload, - MessageReference, ModifyGuildTextChannelOption, ModifyGuildTextChannelPayload, Overwrite, @@ -13,8 +12,6 @@ import { } from '../types/channel.ts' import { CHANNEL, - CHANNEL_MESSAGE, - CHANNEL_MESSAGES, MESSAGE_REACTION_ME, MESSAGE_REACTION_USER } from '../types/endpoint.ts' @@ -62,42 +59,11 @@ export class TextChannel extends Channel { option?: AllMessageOptions, reply?: Message ): Promise { - if (typeof content === 'object') { - option = content - content = undefined - } - if (content === undefined && option === undefined) { - throw new Error('Either text or option is necessary.') - } - if (option instanceof Embed) { - option = { - embed: option - } - } - - const payload: any = { - content: content, - embed: option?.embed, - file: option?.file, - files: option?.files, - tts: option?.tts, - allowed_mentions: option?.allowedMentions - } - - if (reply !== undefined) { - const reference: MessageReference = { - message_id: reply.id, - channel_id: reply.channel.id, - guild_id: reply.guild?.id - } - payload.message_reference = reference - } - - const resp = await this.client.rest.post(CHANNEL_MESSAGES(this.id), payload) - - const res = new Message(this.client, resp, this, this.client.user as any) - await res.mentions.fromPayload(resp) - return res + return this.client.channels.sendMessage( + this, + content, + Object.assign(option ?? {}, { reply }) + ) } /** @@ -111,32 +77,7 @@ export class TextChannel extends Channel { text?: string, option?: MessageOptions ): Promise { - if (text === undefined && option === undefined) { - throw new Error('Either text or option is necessary.') - } - - if (this.client.user === undefined) { - throw new Error('Client user has not initialized.') - } - - const newMsg = await this.client.rest.patch( - CHANNEL_MESSAGE( - this.id, - typeof message === 'string' ? message : message.id - ), - { - content: text, - embed: option?.embed !== undefined ? option.embed.toJSON() : undefined, - // Cannot upload new files with Message - // file: option?.file, - tts: option?.tts, - allowed_mentions: option?.allowedMentions - } - ) - - const res = new Message(this.client, newMsg, this, this.client.user) - await res.mentions.fromPayload(newMsg) - return res + return this.client.channels.editMessage(this, message, text, option) } /** Add a reaction to a Message in this Channel */ diff --git a/src/test/index.ts b/src/test/index.ts index da0d0bb..0143763 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -243,6 +243,20 @@ client.on('messageCreate', async (msg: Message) => { buf += `\n${role.name}` } msg.reply(buf) + } else if (msg.content === '!timer') { + msg.channel.send('3...').then((msg) => { + setTimeout(() => { + msg.edit('2...').then((msg) => { + setTimeout(() => { + msg.edit('1...').then((msg) => { + setTimeout(() => { + msg.edit('ok wut') + }, 1000) + }) + }, 1000) + }) + }, 1000) + }) } }) diff --git a/src/types/channel.ts b/src/types/channel.ts index e1d44e0..7d06156 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -1,5 +1,5 @@ import { Embed } from '../structures/embed.ts' -import { MessageAttachment } from '../structures/message.ts' +import type { Message, MessageAttachment } from '../structures/message.ts' import { EmojiPayload } from './emoji.ts' import { MemberPayload } from './guild.ts' import { UserPayload } from './user.ts' @@ -156,16 +156,26 @@ export interface MessagePayload { stickers?: MessageStickerPayload[] } +export enum AllowedMentionType { + Roles = 'roles', + Users = 'users', + Everyone = 'everyone' +} + +export interface AllowedMentionsPayload { + parse?: AllowedMentionType[] + users?: string[] + roles?: string[] + replied_user?: boolean +} + export interface MessageOptions { tts?: boolean embed?: Embed file?: MessageAttachment files?: MessageAttachment[] - allowedMentions?: { - parse?: 'everyone' | 'users' | 'roles' - roles?: string[] - users?: string[] - } + allowedMentions?: AllowedMentionsPayload + reply?: Message | MessageReference | string } export interface ChannelMention {