diff --git a/README.md b/README.md index f6d6b4b..8efc3ca 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) -Discord Deno API that is easy to use +**An easy to use Discord API Library for Deno** ## Table of Contents - [Usage](#usage) - [Docs](#docs) -- [Maintainers](#maintainers) +- [Maintainer](#maintainer) - [Contributing](#contributing) - [License](#license) @@ -35,7 +35,7 @@ bot.connect(TOKEN, [GatewayIntents.GUILD_MESSAGES]) Not made yet -## Maintainers +## Maintainer [@Helloyunho](https://github.com/Helloyunho) @@ -43,10 +43,10 @@ Not made yet See [the contributing file](CONTRIBUTING.md)! -PRs accepted. +PRs are accepted. Small note: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. ## License -MIT © 2020 Helloyunho +[MIT © 2020 Helloyunho](LICENSE) diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..fa626f6 --- /dev/null +++ b/mod.ts @@ -0,0 +1,55 @@ +export * from './src/gateway/index.ts' +export * from './src/models/client.ts' +export * from './src/models/rest.ts' +export * from './src/models/CacheAdapter.ts' +export * from './src/models/shard.ts' +export * from './src/managers/BaseManager.ts' +export * from './src/managers/BaseChildManager.ts' +export * from './src/managers/ChannelsManager.ts' +export * from './src/managers/EmojisManager.ts' +export * from './src/managers/GatewayCache.ts' +export * from './src/managers/GuildChannelsManager.ts' +export * from './src/managers/GuildsManager.ts' +export * from './src/managers/MembersManager.ts' +export * from './src/managers/MessagesManager.ts' +export * from './src/managers/RolesManager.ts' +export * from './src/managers/UsersManager.ts' +export * from './src/structures/base.ts' +export * from './src/structures/cdn.ts' +export * from './src/structures/channel.ts' +export * from './src/structures/dmChannel.ts' +export * from './src/structures/embed.ts' +export * from './src/structures/emoji.ts' +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' +export * from './src/structures/message.ts' +export * from './src/structures/MessageMentions.ts' +export * from './src/structures/presence.ts' +export * from './src/structures/role.ts' +export * from './src/structures/snowflake.ts' +export * from './src/structures/textChannel.ts' +export * from './src/structures/user.ts' +export * from './src/structures/webhook.ts' +export * from './src/types/cdn.ts' +export * from './src/types/channel.ts' +export * from './src/types/emoji.ts' +export * from './src/types/endpoint.ts' +export * from './src/types/gateway.ts' +export * from './src/types/gatewayBot.ts' +export * from './src/types/gatewayResponse.ts' +export * from './src/types/guild.ts' +export * from './src/types/invite.ts' +export * from './src/types/permissionFlags.ts' +export * from './src/types/presence.ts' +export * from './src/types/role.ts' +export * from './src/types/template.ts' +export * from './src/types/user.ts' +export * from './src/types/voice.ts' +export * from './src/types/webhook.ts' +export * from './src/utils/collection.ts' \ No newline at end of file diff --git a/src/gateway/handlers/channelDelete.ts b/src/gateway/handlers/channelDelete.ts index 494e4cf..37cd2b4 100644 --- a/src/gateway/handlers/channelDelete.ts +++ b/src/gateway/handlers/channelDelete.ts @@ -1,12 +1,11 @@ import { Gateway, GatewayEventHandler } from '../index.ts' -import { Channel } from '../../structures/channel.ts' import { ChannelPayload } from '../../types/channel.ts' export const channelDelete: GatewayEventHandler = async ( gateway: Gateway, d: ChannelPayload ) => { - const channel: Channel = await gateway.client.channels.get(d.id) + const channel = await gateway.client.channels.get(d.id) if (channel !== undefined) { await gateway.client.channels.delete(d.id) gateway.client.emit('channelDelete', channel) diff --git a/src/gateway/handlers/channelPinsUpdate.ts b/src/gateway/handlers/channelPinsUpdate.ts index 745ff1b..da12344 100644 --- a/src/gateway/handlers/channelPinsUpdate.ts +++ b/src/gateway/handlers/channelPinsUpdate.ts @@ -6,7 +6,7 @@ export const channelPinsUpdate: GatewayEventHandler = async ( gateway: Gateway, d: ChannelPinsUpdatePayload ) => { - const after: TextChannel = await gateway.client.channels.get(d.channel_id) + const after: TextChannel | void = await gateway.client.channels.get(d.channel_id) if (after !== undefined) { const before = after.refreshFromData({ last_pin_timestamp: d.last_pin_timestamp diff --git a/src/gateway/handlers/channelUpdate.ts b/src/gateway/handlers/channelUpdate.ts index 9a75e61..869290b 100644 --- a/src/gateway/handlers/channelUpdate.ts +++ b/src/gateway/handlers/channelUpdate.ts @@ -1,4 +1,5 @@ import { Channel } from '../../structures/channel.ts' +import { Guild } from "../../structures/guild.ts" import { ChannelPayload } from '../../types/channel.ts' import getChannelByType from '../../utils/getChannelByType.ts' import { Gateway, GatewayEventHandler } from '../index.ts' @@ -7,12 +8,16 @@ export const channelUpdate: GatewayEventHandler = async ( gateway: Gateway, d: ChannelPayload ) => { - const oldChannel: Channel = await gateway.client.channels.get(d.id) + const oldChannel: Channel | undefined = await gateway.client.channels.get(d.id) if (oldChannel !== undefined) { await gateway.client.channels.set(d.id, d) + let guild: undefined | Guild; + if((d as any).guild_id !== undefined) { + guild = await gateway.client.guilds.get((d as any).guild_id) as Guild | undefined + } if (oldChannel.type !== d.type) { - const channel: Channel = getChannelByType(gateway.client, d) ?? oldChannel + const channel: Channel = getChannelByType(gateway.client, d, guild) ?? oldChannel gateway.client.emit('channelUpdate', oldChannel, channel) } else { const before = oldChannel.refreshFromData(d) diff --git a/src/gateway/handlers/guildBanAdd.ts b/src/gateway/handlers/guildBanAdd.ts index 699b23d..232fc8d 100644 --- a/src/gateway/handlers/guildBanAdd.ts +++ b/src/gateway/handlers/guildBanAdd.ts @@ -13,7 +13,7 @@ export const guildBanAdd: GatewayEventHandler = async ( new User(gateway.client, d.user) if (guild !== undefined) { - guild.members = guild.members?.filter(member => member.id !== d.user.id) + await guild.members.delete(user.id) gateway.client.emit('guildBanAdd', guild, user) } } diff --git a/src/gateway/handlers/guildCreate.ts b/src/gateway/handlers/guildCreate.ts index d885db6..edb858d 100644 --- a/src/gateway/handlers/guildCreate.ts +++ b/src/gateway/handlers/guildCreate.ts @@ -1,19 +1,54 @@ import { Gateway, GatewayEventHandler } from '../index.ts' import { Guild } from '../../structures/guild.ts' -import { GuildPayload } from '../../types/guild.ts' +import { GuildPayload, MemberPayload } from "../../types/guild.ts" +import { MembersManager } from "../../managers/MembersManager.ts" +import { ChannelPayload } from "../../types/channel.ts" +import { RolePayload } from "../../types/role.ts" +import { RolesManager } from "../../managers/RolesManager.ts" -export const guildCreate: GatewayEventHandler = async ( - gateway: Gateway, - d: GuildPayload -) => { +export const guildCreate: GatewayEventHandler = async(gateway: Gateway, d: GuildPayload) => { let guild: Guild | undefined = await gateway.client.guilds.get(d.id) if (guild !== undefined) { // It was just lazy load, so we don't fire the event as its gonna fire for every guild bot is in await gateway.client.guilds.set(d.id, d) + if ((d as any).members !== undefined) { + const members = new MembersManager(gateway.client, guild) + await members.fromPayload((d as any).members as MemberPayload[]) + guild.members = members + } + if ((d as any).channels !== undefined) { + for (const ch of (d as any).channels as ChannelPayload[]) { + (ch as any).guild_id = d.id + await gateway.client.channels.set(ch.id, ch) + } + } + if ((d as any).roles !== undefined) { + const roles = new RolesManager(gateway.client, guild) + await roles.fromPayload((d as any).roles as RolePayload[]) + guild.roles = roles + } guild.refreshFromData(d) } else { await gateway.client.guilds.set(d.id, d) guild = new Guild(gateway.client, d) + if ((d as any).members !== undefined) { + const members = new MembersManager(gateway.client, guild) + await members.fromPayload((d as any).members as MemberPayload[]) + guild.members = members + } + if ((d as any).channels !== undefined) { + for (const ch of (d as any).channels as ChannelPayload[]) { + (ch as any).guild_id = d.id + await gateway.client.channels.set(ch.id, ch) + } + } + if ((d as any).roles !== undefined) { + const roles = new RolesManager(gateway.client, guild) + await roles.fromPayload((d as any).roles as RolePayload[]) + guild.roles = roles + } + await guild.roles.fromPayload(d.roles) + guild = new Guild(gateway.client, d) gateway.client.emit('guildCreate', guild) } } diff --git a/src/gateway/handlers/guildDelete.ts b/src/gateway/handlers/guildDelete.ts index 3db66a7..f9e6db3 100644 --- a/src/gateway/handlers/guildDelete.ts +++ b/src/gateway/handlers/guildDelete.ts @@ -10,6 +10,9 @@ export const guildDelte: GatewayEventHandler = async ( if (guild !== undefined) { guild.refreshFromData(d) + await guild.members.flush() + await guild.channels.flush() + await guild.roles.flush() await gateway.client.guilds.delete(d.id) gateway.client.emit('guildDelete', guild) } diff --git a/src/gateway/handlers/guildEmojiUpdate.ts b/src/gateway/handlers/guildEmojiUpdate.ts index 5d08bab..0bf5c9f 100644 --- a/src/gateway/handlers/guildEmojiUpdate.ts +++ b/src/gateway/handlers/guildEmojiUpdate.ts @@ -1,14 +1,14 @@ import cache from '../../models/cache.ts' import { Guild } from '../../structures/guild.ts' -import { GuildEmojiUpdatePayload } from '../../types/gatewayTypes.ts' +import { GuildEmojiUpdatePayload } from '../../types/gateway.ts' import { Gateway, GatewayEventHandler } from '../index.ts' -const guildEmojiUpdate: GatewayEventHandler = ( +export const guildEmojiUpdate: GatewayEventHandler = ( gateway: Gateway, d: GuildEmojiUpdatePayload ) => { const guild: Guild = cache.get('guild', d.guild_id) if (guild !== undefined) { - const emojis = guild.emojis + // const emojis = guild.emojis } } diff --git a/src/gateway/handlers/index.ts b/src/gateway/handlers/index.ts index efe05a4..00527f4 100644 --- a/src/gateway/handlers/index.ts +++ b/src/gateway/handlers/index.ts @@ -12,12 +12,13 @@ import { ready } from './ready.ts' import { guildBanRemove } from './guildBanRemove.ts' import { messageCreate } from "./messageCreate.ts" import { resume } from "./resume.ts" +import { reconnect } from './reconnect.ts' export const gatewayHandlers: { [eventCode in GatewayEvents]: GatewayEventHandler | undefined } = { READY: ready, - RECONNECT: undefined, + RECONNECT: reconnect, RESUMED: resume, CHANNEL_CREATE: channelCreate, CHANNEL_DELETE: channelDelete, diff --git a/src/gateway/handlers/messageCreate.ts b/src/gateway/handlers/messageCreate.ts index b3dd359..3e124fa 100644 --- a/src/gateway/handlers/messageCreate.ts +++ b/src/gateway/handlers/messageCreate.ts @@ -1,4 +1,3 @@ -import { Channel } from '../../structures/channel.ts' import { Message } from '../../structures/message.ts' import { MessageMentions } from '../../structures/MessageMentions.ts' import { TextChannel } from '../../structures/textChannel.ts' @@ -10,13 +9,18 @@ export const messageCreate: GatewayEventHandler = async ( gateway: Gateway, d: MessagePayload ) => { - let channel = (await gateway.client.channels.get(d.channel_id)) as TextChannel + let channel = await gateway.client.channels.get(d.channel_id) // Fetch the channel if not cached if (channel === undefined) - channel = (await gateway.client.channels.fetch(d.channel_id)) as TextChannel + channel = (await gateway.client.channels.fetch(d.channel_id)) as any const user = new User(gateway.client, d.author) await gateway.client.users.set(d.author.id, d.author) + let guild + if(d.guild_id !== undefined) { + guild = await gateway.client.guilds.get(d.guild_id) + } const mentions = new MessageMentions() - const message = new Message(gateway.client, d, channel, user, mentions) + const message = new Message(gateway.client, d, channel as any, user, mentions) + if (guild !== undefined) message.guild = guild gateway.client.emit('messageCreate', message) } diff --git a/src/gateway/handlers/reconnect.ts b/src/gateway/handlers/reconnect.ts new file mode 100644 index 0000000..149a3ee --- /dev/null +++ b/src/gateway/handlers/reconnect.ts @@ -0,0 +1,6 @@ +import { Gateway } from "../index.ts" +import { GatewayEventHandler } from "../index.ts" + +export const reconnect: GatewayEventHandler = async (gateway: Gateway, d: any) => { + gateway.reconnect() +} \ No newline at end of file diff --git a/src/gateway/handlers/resume.ts b/src/gateway/handlers/resume.ts index 9c9f212..f4948d9 100644 --- a/src/gateway/handlers/resume.ts +++ b/src/gateway/handlers/resume.ts @@ -1,7 +1,10 @@ +import { User } from "../../structures/user.ts" +import { CLIENT_USER } from "../../types/endpoint.ts" import { Gateway, GatewayEventHandler } from '../index.ts' -export const resume: GatewayEventHandler = (gateway: Gateway, d: any) => { +export const resume: GatewayEventHandler = async (gateway: Gateway, d: any) => { gateway.debug(`Session Resumed!`) gateway.client.emit('resume') + if (gateway.client.user === undefined) gateway.client.user = new User(gateway.client, await gateway.client.rest.get(CLIENT_USER()) as any) gateway.client.emit('ready') } \ No newline at end of file diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 4ffbe85..f3de6b8 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -12,7 +12,8 @@ import { } from '../types/gateway.ts' import { gatewayHandlers } from './handlers/index.ts' import { GATEWAY_BOT } from '../types/endpoint.ts' -import { GatewayCache } from '../managers/GatewayCache.ts' +import { GatewayCache } from "../managers/GatewayCache.ts" +import { ClientActivityPayload } from "../structures/presence.ts" /** * Handles Discord gateway connection. @@ -35,7 +36,7 @@ class Gateway { client: Client cache: GatewayCache - constructor (client: Client, token: string, intents: GatewayIntents[]) { + constructor(client: Client, token: string, intents: GatewayIntents[]) { this.token = token this.intents = intents this.client = client @@ -52,12 +53,12 @@ class Gateway { this.websocket.onerror = this.onerror.bind(this) } - private onopen (): void { + private onopen(): void { this.connected = true this.debug('Connected to Gateway!') } - private async onmessage (event: MessageEvent): Promise { + private async onmessage(event: MessageEvent): Promise { let data = event.data if (data instanceof ArrayBuffer) { data = new Uint8Array(data) @@ -85,21 +86,17 @@ class Gateway { return } - this.websocket.send( - JSON.stringify({ - op: GatewayOpcodes.HEARTBEAT, - d: this.sequenceID ?? null - }) - ) + this.send({ + op: GatewayOpcodes.HEARTBEAT, + d: this.sequenceID ?? null + }) this.lastPingTimestamp = Date.now() }, this.heartbeatInterval) if (!this.initialized) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.sendIdentify() this.initialized = true + await this.sendIdentify(this.client.forceNewSession) } else { - console.log('Calling Resume') // eslint-disable-next-line @typescript-eslint/no-floating-promises this.sendResume() } @@ -198,7 +195,7 @@ class Gateway { } } - private onerror (event: Event | ErrorEvent): void { + private onerror(event: Event | ErrorEvent): void { const eventError = event as ErrorEvent console.log(eventError) } @@ -224,34 +221,27 @@ class Gateway { return await this.sendResume() } } - this.websocket.send( - JSON.stringify({ - op: GatewayOpcodes.IDENTIFY, - d: { - token: this.token, - properties: { - $os: Deno.build.os, - $browser: 'discord.deno', - $device: 'discord.deno' - }, - compress: true, - shard: [0, 1], // TODO: Make sharding possible - intents: this.intents.reduce( - (previous, current) => previous | current, - 0 - ), - presence: { - // TODO: User should can customize this - status: 'online', - since: null, - afk: false - } - } - }) - ) + this.send({ + op: GatewayOpcodes.IDENTIFY, + d: { + token: this.token, + properties: { + $os: Deno.build.os, + $browser: 'discord.deno', //TODO: Change lib name + $device: 'discord.deno' + }, + compress: true, + shard: [0, 1], // TODO: Make sharding possible + intents: this.intents.reduce( + (previous, current) => previous | current, + 0 + ), + presence: this.client.presence.create() + } + }) } - private async sendResume (): Promise { + private async sendResume(): Promise { this.debug(`Preparing to resume with Session: ${this.sessionID}`) if (this.sequenceID === undefined) { const cached = await this.cache.get('seq') @@ -266,7 +256,7 @@ class Gateway { seq: this.sequenceID ?? null } } - this.websocket.send(JSON.stringify(resumePayload)) + this.send(resumePayload) } debug (msg: string): void { @@ -281,7 +271,7 @@ class Gateway { this.initWebsocket() } - initWebsocket (): void { + initWebsocket(): void { this.websocket = new WebSocket( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `${DISCORD_GATEWAY_URL}/?v=${DISCORD_API_VERSION}&encoding=json`, @@ -294,9 +284,27 @@ class Gateway { this.websocket.onerror = this.onerror.bind(this) } - close (): void { + close(): void { this.websocket.close(1000) } + + send(data: GatewayResponse): boolean { + if (this.websocket.readyState !== this.websocket.OPEN) return false + this.websocket.send(JSON.stringify({ + op: data.op, + d: data.d, + s: typeof data.s === "number" ? data.s : null, + t: data.t === undefined ? null : data.t, + })) + return true + } + + sendPresence(data: ClientActivityPayload): void { + this.send({ + op: GatewayOpcodes.PRESENCE_UPDATE, + d: data + }) + } } export type GatewayEventHandler = (gateway: Gateway, d: any) => void diff --git a/src/managers/BaseChildManager.ts b/src/managers/BaseChildManager.ts new file mode 100644 index 0000000..6f6c51e --- /dev/null +++ b/src/managers/BaseChildManager.ts @@ -0,0 +1,40 @@ +import { Client } from "../models/client.ts"; +import { Collection } from "../utils/collection.ts"; +import { BaseManager } from "./BaseManager.ts"; + +export class BaseChildManager { + client: Client + parent: BaseManager + + constructor(client: Client, parent: BaseManager) { + this.client = client + this.parent = parent + } + + async get(key: string): Promise { + return this.parent.get(key) + } + + async set(key: string, value: T): Promise { + return this.parent.set(key, value) + } + + async delete(key: string): Promise { + return false + } + + async array(): Promise { + return this.parent.array() + } + + async collection(): Promise> { + const arr = await this.array() as undefined | T2[] + if (arr === undefined) return new Collection() + const collection = new Collection() + for (const elem of arr) { + // @ts-expect-error + collection.set(elem.id, elem) + } + return collection + } +} \ No newline at end of file diff --git a/src/managers/BaseManager.ts b/src/managers/BaseManager.ts index 921cdd5..0037e22 100644 --- a/src/managers/BaseManager.ts +++ b/src/managers/BaseManager.ts @@ -1,4 +1,5 @@ -import { Client } from '../models/client.ts' +import { Client } from "../models/client.ts"; +import { Collection } from "../utils/collection.ts"; export class BaseManager { client: Client @@ -28,4 +29,24 @@ export class BaseManager { async delete (key: string): Promise { return this.client.cache.delete(this.cacheName, key) } + + async array (): Promise { + const arr = await (this.client.cache.array(this.cacheName) as T[]) + return arr.map(e => new this.DataType(this.client, e)) as any + } + + async collection (): Promise> { + const arr = await this.array() + if (arr === undefined) return new Collection() + const collection = new Collection() + for (const elem of arr) { + // @ts-expect-error + collection.set(elem.id, elem) + } + return collection + } + + flush(): any { + return this.client.cache.deleteCache(this.cacheName) + } } diff --git a/src/managers/ChannelsManager.ts b/src/managers/ChannelsManager.ts index 0cea8dd..159cefd 100644 --- a/src/managers/ChannelsManager.ts +++ b/src/managers/ChannelsManager.ts @@ -1,29 +1,50 @@ -import { Client } from '../models/client.ts' -import { Channel } from '../structures/channel.ts' -import { User } from '../structures/user.ts' -import { ChannelPayload } from '../types/channel.ts' -import { CHANNEL } from '../types/endpoint.ts' -import { BaseManager } from './BaseManager.ts' +import { Client } from "../models/client.ts"; +import { Channel } from "../structures/channel.ts"; +import { ChannelPayload } from "../types/channel.ts"; +import { CHANNEL } from "../types/endpoint.ts"; +import getChannelByType from "../utils/getChannelByType.ts"; +import { BaseManager } from "./BaseManager.ts"; export class ChannelsManager extends BaseManager { - constructor (client: Client) { - super(client, 'channels', User) + constructor(client: Client) { + super(client, "channels", Channel) } // Override get method as Generic - async get (key: string): Promise { - return new this.DataType(this.client, this._get(key)) + async get(key: string): Promise { + const data = await this._get(key) + if (data === undefined) return + let guild + if ((data as any).guild_id !== undefined) { + guild = await this.client.guilds.get((data as any).guild_id) + } + const res = getChannelByType(this.client, data, guild) + return res as any } - async fetch (id: string): Promise { + async array(): Promise { + const arr = await (this.client.cache.array(this.cacheName) as ChannelPayload[]) + const result: any[] = [] + for (const elem of arr) { + let guild + if ((elem as any).guild_id !== undefined) { + guild = await this.client.guilds.get((elem as any).guild_id) + } + result.push(getChannelByType(this.client, elem, guild)) + } + return result + } + + async fetch(id: string): Promise { return await new Promise((resolve, reject) => { - this.client.rest - .get(CHANNEL(id)) - .then(data => { - this.set(id, data as ChannelPayload) - resolve(new Channel(this.client, data as ChannelPayload)) - }) - .catch(e => reject(e)) + this.client.rest.get(CHANNEL(id)).then(async data => { + this.set(id, data as ChannelPayload) + let guild + if (data.guild_id !== undefined) { + guild = await this.client.guilds.get(data.guild_id) + } + resolve(getChannelByType(this.client, data as ChannelPayload, guild)) + }).catch(e => reject(e)) }) } } diff --git a/src/managers/GuildChannelsManager.ts b/src/managers/GuildChannelsManager.ts new file mode 100644 index 0000000..a7405b8 --- /dev/null +++ b/src/managers/GuildChannelsManager.ts @@ -0,0 +1,45 @@ +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 { VoiceChannel } from "../structures/guildVoiceChannel.ts"; +import { GuildChannelCategoryPayload, GuildTextChannelPayload, GuildVoiceChannelPayload } from "../types/channel.ts"; +import { CHANNEL } from "../types/endpoint.ts"; +import { BaseChildManager } from "./BaseChildManager.ts"; +import { ChannelsManager } from "./ChannelsManager.ts"; + +export type GuildChannelPayloads = GuildTextChannelPayload | GuildVoiceChannelPayload | GuildChannelCategoryPayload +export type GuildChannel = GuildTextChannel | VoiceChannel | CategoryChannel + +export class GuildChannelsManager extends BaseChildManager { + guild: Guild + + constructor(client: Client, parent: ChannelsManager, guild: Guild) { + super(client, parent as any) + this.guild = guild + } + + async get(id: string): Promise { + const res = await this.parent.get(id) + if (res !== undefined && res.guild.id === this.guild.id) return res + else return undefined + } + + async delete(id: string): Promise { + return this.client.rest.delete(CHANNEL(id)) + } + + async array(): Promise { + const arr = await this.parent.array() as Channel[] + return arr.filter((c: any) => c.guild !== undefined && c.guild.id === this.guild.id) as any + } + + async flush(): Promise { + const arr = await this.array() + for (const elem of arr) { + this.parent.delete(elem.id) + } + return true + } +} \ No newline at end of file diff --git a/src/managers/GuildsManager.ts b/src/managers/GuildsManager.ts index 5d78002..6d29571 100644 --- a/src/managers/GuildsManager.ts +++ b/src/managers/GuildsManager.ts @@ -1,23 +1,27 @@ -import { Client } from '../models/client.ts' -import { Guild } from '../structures/guild.ts' -import { GUILD } from '../types/endpoint.ts' -import { GuildPayload } from '../types/guild.ts' -import { BaseManager } from './BaseManager.ts' +import { Client } from "../models/client.ts"; +import { Guild } from "../structures/guild.ts"; +import { GUILD } from "../types/endpoint.ts"; +import { GuildPayload, MemberPayload } from "../types/guild.ts"; +import { BaseManager } from "./BaseManager.ts"; +import { MembersManager } from "./MembersManager.ts"; export class GuildManager extends BaseManager { constructor (client: Client) { super(client, 'guilds', Guild) } - async fetch (id: string): Promise { + async fetch(id: string): Promise { return await new Promise((resolve, reject) => { - this.client.rest - .get(GUILD(id)) - .then(data => { - this.set(id, data as GuildPayload) - resolve(new Guild(this.client, data as GuildPayload)) - }) - .catch(e => reject(e)) + this.client.rest.get(GUILD(id)).then(async (data: any) => { + this.set(id, data) + const guild = new Guild(this.client, data) + if ((data as GuildPayload).members !== undefined) { + const members = new MembersManager(this.client, guild) + await members.fromPayload((data as GuildPayload).members as MemberPayload[]) + guild.members = members + } + resolve(guild) + }).catch(e => reject(e)) }) } } diff --git a/src/managers/MembersManager.ts b/src/managers/MembersManager.ts new file mode 100644 index 0000000..964749a --- /dev/null +++ b/src/managers/MembersManager.ts @@ -0,0 +1,30 @@ +import { Client } from "../models/client.ts"; +import { Guild } from "../structures/guild.ts"; +import { Member } from "../structures/member.ts"; +import { GUILD_MEMBER } from "../types/endpoint.ts"; +import { MemberPayload } from "../types/guild.ts"; +import { BaseManager } from "./BaseManager.ts"; + +export class MembersManager extends BaseManager { + guild: Guild + + constructor(client: Client, guild: Guild) { + super(client, `members:${guild.id}`, Member) + this.guild = guild + } + + async fetch(id: string): Promise { + return await new Promise((resolve, reject) => { + this.client.rest.get(GUILD_MEMBER(this.guild.id, id)).then(data => { + this.set(id, data as MemberPayload) + resolve(new Member(this.client, data as MemberPayload)) + }).catch(e => reject(e)) + }) + } + + async fromPayload(members: MemberPayload[]): Promise { + for (const member of members) { + await this.set(member.user.id, member) + } + } +} \ No newline at end of file diff --git a/src/managers/MessagesManager.ts b/src/managers/MessagesManager.ts index 2843d92..0e6aa5e 100644 --- a/src/managers/MessagesManager.ts +++ b/src/managers/MessagesManager.ts @@ -1,43 +1,40 @@ -import { Client } from '../models/client.ts' -import { Message } from '../structures/message.ts' -import { MessageMentions } from '../structures/MessageMentions.ts' -import { User } from '../structures/user.ts' -import { MessagePayload } from '../types/channel.ts' -import { CHANNEL_MESSAGE } from '../types/endpoint.ts' -import { BaseManager } from './BaseManager.ts' +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"; +import { CHANNEL_MESSAGE } from "../types/endpoint.ts"; +import { BaseManager } from "./BaseManager.ts"; export class MessagesManager extends BaseManager { constructor (client: Client) { super(client, 'messages', Message) } - async fetch (channelID: string, id: string): Promise { + async get(key: string): Promise { + const raw = await this._get(key) + if (raw === undefined) return + let channel = await this.client.channels.get(raw.channel_id) + if (channel === undefined) channel = await this.client.channels.fetch(raw.channel_id) + if (channel === undefined) return + const author = new User(this.client, raw.author) + const mentions = new MessageMentions() + return new this.DataType(this.client, raw, channel, author, mentions) as any + } + + async fetch(channelID: string, id: string): Promise { return await new Promise((resolve, reject) => { - this.client.rest - .get(CHANNEL_MESSAGE(channelID, id)) - .then(async data => { - this.set(id, data as MessagePayload) - let channel = await this.client.channels.get(channelID) - if (channel === undefined) - channel = await this.client.channels.fetch(channelID) - const author = new User(this.client, (data as MessagePayload).author) - await this.client.users.set( - author.id, - (data as MessagePayload).author - ) - // TODO: Make this thing work (MessageMentions) - const mentions = new MessageMentions() - resolve( - new Message( - this.client, - data as MessagePayload, - channel, - author, - mentions - ) - ) - }) - .catch(e => reject(e)) + this.client.rest.get(CHANNEL_MESSAGE(channelID, id)).then(async data => { + this.set(id, data as MessagePayload) + let channel: any = await this.client.channels.get(channelID) + if (channel === undefined) channel = await this.client.channels.fetch(channelID) + const author = new User(this.client, (data as MessagePayload).author) + await this.client.users.set(author.id, (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)) + }).catch(e => reject(e)) }) } } diff --git a/src/managers/RolesManager.ts b/src/managers/RolesManager.ts index 6bf0644..a6e1d44 100644 --- a/src/managers/RolesManager.ts +++ b/src/managers/RolesManager.ts @@ -24,4 +24,11 @@ export class RolesManager extends BaseManager { .catch(e => reject(e)) }) } + + async fromPayload(roles: RolePayload[]): Promise { + for (const role of roles) { + await this.set(role.id, role) + } + return true + } } diff --git a/src/models/CacheAdapter.ts b/src/models/CacheAdapter.ts index 4b1e14d..6355622 100644 --- a/src/models/CacheAdapter.ts +++ b/src/models/CacheAdapter.ts @@ -8,10 +8,11 @@ import { export interface ICacheAdapter { client: Client - get: (cacheName: string, key: string) => Promise - set: (cacheName: string, key: string, value: any) => Promise - delete: (cacheName: string, key: string) => Promise - array: (cacheName: string) => Promise + get: (cacheName: string, key: string) => Promise | any + set: (cacheName: string, key: string, value: any) => Promise | any + delete: (cacheName: string, key: string) => Promise | boolean + array: (cacheName: string) => undefined | any[] | Promise + deleteCache: (cacheName: string) => any } export class DefaultCacheAdapter implements ICacheAdapter { @@ -50,6 +51,11 @@ export class DefaultCacheAdapter implements ICacheAdapter { if (cache === undefined) return return cache.array() } + + async deleteCache(cacheName: string): Promise { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + return delete this.data[cacheName] + } } export class RedisCacheAdapter implements ICacheAdapter { @@ -114,4 +120,9 @@ export class RedisCacheAdapter implements ICacheAdapter { const data = await this.redis?.hvals(cacheName) return data?.map((e: string) => JSON.parse(e)) } + + async deleteCache(cacheName: string): Promise { + await this._checkReady() + return await this.redis?.del(cacheName) !== 0 + } } diff --git a/src/models/client.ts b/src/models/client.ts index bce377a..09d1750 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -3,18 +3,21 @@ import { GatewayIntents } from '../types/gateway.ts' import { Gateway } from '../gateway/index.ts' import { RESTManager } from './rest.ts' import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts' -import { DefaultCacheAdapter, ICacheAdapter } from './CacheAdapter.ts' -import { UserManager } from '../managers/UsersManager.ts' -import { GuildManager } from '../managers/GuildsManager.ts' -import { EmojisManager } from '../managers/EmojisManager.ts' -import { ChannelsManager } from '../managers/ChannelsManager.ts' -import { MessagesManager } from '../managers/MessagesManager.ts' +import { DefaultCacheAdapter, ICacheAdapter } from "./CacheAdapter.ts" +import { UserManager } from "../managers/UsersManager.ts" +import { GuildManager } from "../managers/GuildsManager.ts" +import { EmojisManager } from "../managers/EmojisManager.ts" +import { ChannelsManager } from "../managers/ChannelsManager.ts" +import { MessagesManager } from "../managers/MessagesManager.ts" +import { ActivityGame, ClientActivity, ClientPresence } from "../structures/presence.ts" /** Some Client Options to modify behaviour */ export interface ClientOptions { token?: string intents?: GatewayIntents[] - cache?: ICacheAdapter + cache?: ICacheAdapter, + forceNewSession?: boolean, + presence?: ClientPresence | ClientActivity | ActivityGame } /** @@ -28,18 +31,22 @@ export class Client extends EventEmitter { token?: string cache: ICacheAdapter = new DefaultCacheAdapter(this) intents?: GatewayIntents[] - + forceNewSession?: boolean users: UserManager = new UserManager(this) guilds: GuildManager = new GuildManager(this) channels: ChannelsManager = new ChannelsManager(this) messages: MessagesManager = new MessagesManager(this) emojis: EmojisManager = new EmojisManager(this) + presence: ClientPresence = new ClientPresence() + constructor (options: ClientOptions = {}) { super() this.token = options.token this.intents = options.intents + this.forceNewSession = options.forceNewSession if (options.cache !== undefined) this.cache = options.cache + if (options.presence !== undefined) this.presence = options.presence instanceof ClientPresence ? options.presence : new ClientPresence(options.presence) } setAdapter (adapter: ICacheAdapter): Client { @@ -47,8 +54,15 @@ export class Client extends EventEmitter { return this } + setPresence (presence: ClientPresence | ClientActivity | ActivityGame): void { + if (presence instanceof ClientPresence) { + this.presence = presence + } else this.presence = new ClientPresence(presence) + this.gateway?.sendPresence(this.presence.create()) + } + debug (tag: string, msg: string): void { - this.emit('debug', `[${tag}] ${msg}`) + this.emit("debug", `[${tag}] ${msg}`) } /** diff --git a/src/models/rest.ts b/src/models/rest.ts index 8632e7e..36f34b4 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -52,7 +52,7 @@ export class RESTManager { constructor (client: Client) { this.client = client - setTimeout(this.processRateLimitedPaths, 1000) + setTimeout(() => this.processRateLimitedPaths, 1000) } async processRateLimitedPaths (): Promise { @@ -158,7 +158,7 @@ export class RESTManager { 'User-Agent': `DiscordBot (discord.deno)` } - if (this.client.token !== undefined) delete headers.Authorization + if (this.client.token === undefined) delete headers.Authorization if (method === 'get') body = undefined @@ -233,9 +233,11 @@ export class RESTManager { const urlToUse = method === 'get' && query !== '' ? `${url}?${query}` : url + const requestData = this.createRequestBody(body, method) + const response = await fetch( urlToUse, - this.createRequestBody(body, method) + requestData ) const bucketIDFromHeaders = this.processHeaders(url, response.headers) this.handleStatusCode(response, errorStack) @@ -302,15 +304,17 @@ export class RESTManager { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.logErrors(response, errorStack) + if(status === HttpResponseCode.Unauthorized) throw new Error("Request was not successful. Invalid Token.") + switch (status) { case HttpResponseCode.BadRequest: case HttpResponseCode.Unauthorized: case HttpResponseCode.Forbidden: case HttpResponseCode.NotFound: case HttpResponseCode.MethodNotAllowed: - throw new Error('Request Client Error') + throw new Error('Request Client Error.') case HttpResponseCode.GatewayUnavailable: - throw new Error('Request Server Error') + throw new Error('Request Server Error.') } // left are all unknown diff --git a/src/structures/dmChannel.ts b/src/structures/dmChannel.ts index f1ee30f..1513216 100644 --- a/src/structures/dmChannel.ts +++ b/src/structures/dmChannel.ts @@ -9,7 +9,6 @@ export class DMChannel extends TextChannel { constructor (client: Client, data: DMChannelPayload) { super(client, data) this.recipients = data.recipients - // cache.set('dmchannel', this.id, this) } protected readFromData (data: DMChannelPayload): void { diff --git a/src/structures/guild.ts b/src/structures/guild.ts index 15b1156..7dc629f 100644 --- a/src/structures/guild.ts +++ b/src/structures/guild.ts @@ -2,13 +2,12 @@ import { Client } from '../models/client.ts' import { GuildFeatures, GuildPayload } from '../types/guild.ts' import { PresenceUpdatePayload } from '../types/gateway.ts' import { Base } from './base.ts' -import { Channel } from './channel.ts' import { Emoji } from './emoji.ts' -import { Member } from './member.ts' import { VoiceState } from './voiceState.ts' import cache from '../models/cache.ts' -import getChannelByType from '../utils/getChannelByType.ts' -import { RolesManager } from '../managers/RolesManager.ts' +import { RolesManager } from "../managers/RolesManager.ts" +import { GuildChannelsManager } from "../managers/GuildChannelsManager.ts" +import { MembersManager } from "../managers/MembersManager.ts" export class Guild extends Base { id: string @@ -28,7 +27,7 @@ export class Guild extends Base { verificationLevel?: string defaultMessageNotifications?: string explicitContentFilter?: string - roles: RolesManager = new RolesManager(this.client, this) + roles: RolesManager emojis?: Emoji[] features?: GuildFeatures[] mfaLevel?: string @@ -41,8 +40,8 @@ export class Guild extends Base { unavailable: boolean memberCount?: number voiceStates?: VoiceState[] - members?: Member[] - channels?: Channel[] + members: MembersManager + channels: GuildChannelsManager presences?: PresenceUpdatePayload[] maxPresences?: number maxMembers?: number @@ -61,6 +60,9 @@ export class Guild extends Base { super(client, data) this.id = data.id this.unavailable = data.unavailable + this.members = new MembersManager(this.client, this) + this.channels = new GuildChannelsManager(this.client, this.client.channels, this) + this.roles = new RolesManager(this.client, this) if (!this.unavailable) { this.name = data.name @@ -167,22 +169,22 @@ export class Guild extends Base { this.joinedAt = data.joined_at ?? this.joinedAt this.large = data.large ?? this.large this.memberCount = data.member_count ?? this.memberCount - this.voiceStates = - data.voice_states?.map( - v => - cache.get('voiceState', `${v.guild_id}:${v.user_id}`) ?? - new VoiceState(this.client, v) - ) ?? this.voiceStates - this.members = - data.members?.map( - v => - cache.get('member', `${this.id}:${v.user.id}`) ?? - new Member(this.client, v) - ) ?? this.members - this.channels = - data.channels?.map( - v => cache.get('channel', v.id) ?? getChannelByType(this.client, v) - ) ?? this.members + // this.voiceStates = + // data.voice_states?.map( + // v => + // cache.get('voiceState', `${v.guild_id}:${v.user_id}`) ?? + // new VoiceState(this.client, v) + // ) ?? this.voiceStates + // this.members = + // data.members?.map( + // v => + // cache.get('member', `${this.id}:${v.user.id}`) ?? + // new Member(this.client, v) + // ) ?? this.members + // this.channels = + // data.channels?.map( + // v => cache.get('channel', v.id) ?? getChannelByType(this.client, v, this) + // ) ?? this.members this.presences = data.presences ?? this.presences this.maxPresences = data.max_presences ?? this.maxPresences this.maxMembers = data.max_members ?? this.maxMembers diff --git a/src/structures/guildCategoryChannel.ts b/src/structures/guildCategoryChannel.ts index 15f9f8f..741dccc 100644 --- a/src/structures/guildCategoryChannel.ts +++ b/src/structures/guildCategoryChannel.ts @@ -1,6 +1,10 @@ import { Client } from '../models/client.ts' import { Channel } from './channel.ts' -import { GuildChannelCategoryPayload, Overwrite } from '../types/channel.ts' +import { + GuildChannelCategoryPayload, + Overwrite +} from '../types/channel.ts' +import { Guild } from "./guild.ts" export class CategoryChannel extends Channel { guildID: string @@ -8,12 +12,14 @@ export class CategoryChannel extends Channel { position: number permissionOverwrites: Overwrite[] nsfw: boolean + guild: Guild parentID?: string - constructor (client: Client, data: GuildChannelCategoryPayload) { + constructor (client: Client, data: GuildChannelCategoryPayload, 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 diff --git a/src/structures/guildTextChannel.ts b/src/structures/guildTextChannel.ts index 49ae7a2..bd25b21 100644 --- a/src/structures/guildTextChannel.ts +++ b/src/structures/guildTextChannel.ts @@ -1,6 +1,7 @@ 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 @@ -11,15 +12,17 @@ export class GuildTextChannel extends TextChannel { parentID?: string rateLimit: number topic?: string + guild: Guild get mention (): string { return `<#${this.id}>` } - constructor (client: Client, data: GuildTextChannelPayload) { + 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 diff --git a/src/structures/guildVoiceChannel.ts b/src/structures/guildVoiceChannel.ts index dc7f7ba..ed402ff 100644 --- a/src/structures/guildVoiceChannel.ts +++ b/src/structures/guildVoiceChannel.ts @@ -1,24 +1,27 @@ import { Client } from '../models/client.ts' import { GuildVoiceChannelPayload, Overwrite } from '../types/channel.ts' import { Channel } from './channel.ts' +import { Guild } from "./guild.ts" export class VoiceChannel extends Channel { bitrate: string userLimit: number guildID: string name: string + guild: Guild position: number permissionOverwrites: Overwrite[] nsfw: boolean parentID?: string - constructor (client: Client, data: GuildVoiceChannelPayload) { + constructor (client: Client, data: GuildVoiceChannelPayload, guild: Guild) { super(client, data) this.bitrate = data.bitrate this.userLimit = data.user_limit this.guildID = data.guild_id this.name = data.name this.position = data.position + this.guild = guild this.permissionOverwrites = data.permission_overwrites this.nsfw = data.nsfw this.parentID = data.parent_id diff --git a/src/structures/guildnewsChannel.ts b/src/structures/guildnewsChannel.ts index 49786ea..5dcc78d 100644 --- a/src/structures/guildnewsChannel.ts +++ b/src/structures/guildnewsChannel.ts @@ -1,9 +1,11 @@ import { Client } from '../models/client.ts' import { GuildNewsChannelPayload, Overwrite } from '../types/channel.ts' +import { Guild } from "./guild.ts" import { TextChannel } from './textChannel.ts' export class NewsChannel extends TextChannel { guildID: string + guild: Guild name: string position: number permissionOverwrites: Overwrite[] @@ -11,10 +13,11 @@ export class NewsChannel extends TextChannel { parentID?: string topic?: string - constructor (client: Client, data: GuildNewsChannelPayload) { + constructor (client: Client, data: GuildNewsChannelPayload, 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 diff --git a/src/structures/message.ts b/src/structures/message.ts index b0f9be4..aea39bc 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -14,17 +14,19 @@ import { User } from './user.ts' import { Member } from './member.ts' import { Embed } from './embed.ts' import { CHANNEL_MESSAGE } from '../types/endpoint.ts' -import { Channel } from './channel.ts' -import { MessageMentions } from './MessageMentions.ts' -import { TextChannel } from './textChannel.ts' +import { MessageMentions } from "./MessageMentions.ts" +import { TextChannel } from "./textChannel.ts" +import { DMChannel } from "./dmChannel.ts" +import { Guild } from "./guild.ts" export class Message extends Base { // eslint-disable-next-line @typescript-eslint/prefer-readonly private data: MessagePayload id: string channelID: string - channel: Channel + channel: TextChannel guildID?: string + guild?: Guild author: User member?: Member content: string @@ -50,7 +52,7 @@ export class Message extends Base { constructor ( client: Client, data: MessagePayload, - channel: Channel, + channel: TextChannel, author: User, mentions: MessageMentions ) { @@ -122,9 +124,13 @@ export class Message extends Base { } async edit (text?: string, option?: MessageOption): Promise { - // Seriously eslint? - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - return (this.channel as TextChannel).editMessage(this.id, text, option) + return this.channel.edit(this.id, text, option) + } + + async reply(text: string, options?: MessageOption): 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) } async delete (): Promise { diff --git a/src/structures/presence.ts b/src/structures/presence.ts new file mode 100644 index 0000000..614b1c9 --- /dev/null +++ b/src/structures/presence.ts @@ -0,0 +1,123 @@ +export type ActivityType = 'PLAYING' | 'STREAMING' | 'LISTENING' | 'WATCHING' | 'CUSTOM_STATUS' | 'COMPETING'; +export type StatusType = 'online' | 'invisible' | 'offline' | 'idle' | 'dnd'; + +export enum ActivityTypes { + PLAYING = 0, + STREAMING = 1, + LISTENING = 2, + WATCHING = 3, + CUSTOM_STATUS = 4, + COMPETING = 5, +} + +export interface ActivityGame { + name: string; + type: 0 | 1 | 2 | 3 | 4 | 5 | ActivityType; + url?: string; +} + +export interface ClientActivity { + status?: StatusType + activity?: ActivityGame | ActivityGame[] + since?: number | null + afk?: boolean +} + +export interface ClientActivityPayload { + status: StatusType + activities: ActivityGame[] | null + since: number | null + afk: boolean +} + +export class ClientPresence { + status: StatusType = 'online' + activity?: ActivityGame | ActivityGame[] + since?: number | null + afk?: boolean + + constructor(data?: ClientActivity | ClientActivityPayload | ActivityGame) { + if (data !== undefined) { + if ((data as ClientActivity).activity !== undefined) { + Object.assign(this, data) + } else if ((data as ClientActivityPayload).activities !== undefined) { + + } else if ((data as ActivityGame).name !== undefined) { + if (this.activity === undefined) { + this.activity = data as ActivityGame + } else if (this.activity instanceof Array) { + this.activity.push(data as ActivityGame) + } else this.activity = [ this.activity, data as ActivityGame ] + } + } + } + + parse(payload: ClientActivityPayload): ClientPresence { + this.afk = payload.afk + this.activity = payload.activities ?? undefined + this.since = payload.since + this.status = payload.status + return this + } + + static parse(payload: ClientActivityPayload): ClientPresence { + return new ClientPresence().parse(payload) + } + + create(): ClientActivityPayload { + 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 + } + } + + createActivity(): ActivityGame[] | null { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + const activity = this.activity === undefined ? null : (this.activity instanceof Array ? this.activity : [this.activity]) || null + if (activity === null) return activity + else { + activity.map(e => { + if (typeof e.type === "string") e.type = ActivityTypes[e.type] + return e + }) + return activity + } + } + + setStatus(status: StatusType): ClientPresence { + this.status = status + return this + } + + setActivity(activity: ActivityGame): ClientPresence { + this.activity = activity + return this + } + + setActivities(activities: ActivityGame[]): ClientPresence { + this.activity = activities + return this + } + + setAFK(afk: boolean): ClientPresence { + this.afk = afk + return this + } + + removeAFK(): ClientPresence { + this.afk = false + return this + } + + toggleAFK(): ClientPresence { + this.afk = this.afk === undefined ? true : !this.afk + return this + } + + setSince(since?: number): ClientPresence { + this.since = since + return this + } +} \ No newline at end of file diff --git a/src/structures/textChannel.ts b/src/structures/textChannel.ts index bcc9522..ee06f28 100644 --- a/src/structures/textChannel.ts +++ b/src/structures/textChannel.ts @@ -27,35 +27,18 @@ export class TextChannel extends Channel { 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 resp = await fetch(CHANNEL_MESSAGES(this.id), { - headers: { - Authorization: `Bot ${this.client.token}`, - 'Content-Type': 'application/json' - }, - method: 'POST', - body: JSON.stringify({ + const resp = await this.client.rest.post(CHANNEL_MESSAGES(this.id), { content: text, embed: option?.embed, file: option?.file, tts: option?.tts, allowed_mentions: option?.allowedMention - }) }) - return new Message( - this.client, - await resp.json(), - this, - this.client.user, - new MessageMentions() - ) + return new Message(this.client, resp as any, this, this.client.user as any, new MessageMentions()) } - async editMessage ( + async edit ( message: Message | string, text?: string, option?: MessageOption diff --git a/src/test/index.ts b/src/test/index.ts index 971a2d4..a928838 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,15 +1,21 @@ import { Client } from '../models/client.ts' import { GatewayIntents } from '../types/gateway.ts' import { TOKEN } from './config.ts' -import { Channel } from '../structures/channel.ts' -import { GuildTextChannel } from '../structures/guildTextChannel.ts' -import { TextChannel } from '../structures/textChannel.ts' -import { Guild } from '../structures/guild.ts' -import { User } from '../structures/user.ts' import { Message } from "../structures/message.ts" import { RedisCacheAdapter } from "../models/CacheAdapter.ts" +import { ClientPresence } from "../structures/presence.ts" +import { Member } from "../structures/member.ts" +import { Role } from "../structures/role.ts" +import { GuildChannel } from "../managers/GuildChannelsManager.ts" -const bot = new Client() +const bot = new Client({ + presence: new ClientPresence({ + activity: { + name: "Testing", + type: 'COMPETING' + } + }), +}) bot.setAdapter(new RedisCacheAdapter(bot, { hostname: "127.0.0.1", @@ -18,60 +24,44 @@ bot.setAdapter(new RedisCacheAdapter(bot, { bot.on('ready', () => { console.log(`[Login] Logged in as ${bot.user?.tag}!`) + bot.setPresence({ + name: "Test After Ready", + type: 'COMPETING' + }) }) bot.on('debug', console.log) -bot.on('channelDelete', (channel: Channel) => { - console.log('channelDelete', channel.id) -}) - -bot.on('channelUpdate', (before: Channel, after: Channel) => { - if (before instanceof GuildTextChannel && after instanceof GuildTextChannel) { - console.log('channelUpdate', before.name) - console.log('channelUpdate', after.name) - } else { - console.log('channelUpdate', before.id) - console.log('channelUpdate', after.id) +bot.on('messageCreate', async (msg: Message) => { + if (msg.author.bot === true) return + if (msg.content === "!ping") { + msg.reply(`Pong! Ping: ${bot.ping}ms`) + } else if (msg.content === "!members") { + const col = await msg.guild?.members.collection() + const data = col?.array().map((c: Member, i: number) => { + return `${i + 1}. ${c.user.tag}` + }).join("\n") as string + msg.channel.send("Member List:\n" + data) + } else if (msg.content === "!guilds") { + const guilds = await msg.client.guilds.collection() + msg.channel.send("Guild List:\n" + (guilds.array().map((c, i: number) => { + return `${i + 1}. ${c.name} - ${c.memberCount} members` + }).join("\n") as string)) + } else if (msg.content === "!roles") { + const col = await msg.guild?.roles.collection() + const data = col?.array().map((c: Role, i: number) => { + return `${i + 1}. ${c.name}` + }).join("\n") as string + msg.channel.send("Roles List:\n" + data) + } else if (msg.content === "!channels") { + const col = await msg.guild?.channels.array() + const data = col?.map((c: GuildChannel, i: number) => { + return `${i + 1}. ${c.name}` + }).join("\n") as string + msg.channel.send("Channels List:\n" + data) } }) -bot.on('channelCreate', (channel: Channel) => { - console.log('channelCreate', channel.id) -}) - -bot.on('channelPinsUpdate', (before: TextChannel, after: TextChannel) => { - console.log( - 'channelPinsUpdate', - before.lastPinTimestamp, - after.lastPinTimestamp - ) -}) - -bot.on('guildBanAdd', (guild: Guild, user: User) => { - console.log('guildBanAdd', guild.id, user.id) -}) - -bot.on('guildBanRemove', (guild: Guild, user: User) => { - console.log('guildBanRemove', guild.id, user.id) -}) - -bot.on('guildCreate', (guild: Guild) => { - console.log('guildCreate', guild.id) -}) - -bot.on('guildDelete', (guild: Guild) => { - console.log('guildDelete', guild.id) -}) - -bot.on('guildUpdate', (before: Guild, after: Guild) => { - console.log('guildUpdate', before.name, after.name) -}) - -bot.on('messageCreate', (msg: Message) => { - console.log(`${msg.author.tag}: ${msg.content}`) -}) - bot.connect(TOKEN, [ GatewayIntents.GUILD_MEMBERS, GatewayIntents.GUILD_PRESENCES, diff --git a/src/types/endpoint.ts b/src/types/endpoint.ts index 77e2f0e..c8ccdfe 100644 --- a/src/types/endpoint.ts +++ b/src/types/endpoint.ts @@ -187,6 +187,10 @@ const INVITE = (inviteCODE: string): string => const VOICE_REGIONS = (guildID: string): string => `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/guilds/${guildID}/regions` +// Client User Endpoint +const CLIENT_USER = (): string => + `${DISCORD_API_URL}/v${DISCORD_API_VERSION}/users/@me` + export default [ GUILDS, GUILD, @@ -204,6 +208,7 @@ export default [ GUILD_CHANNEL, GUILD_CHANNELS, GUILD_MEMBER, + CLIENT_USER, GUILD_MEMBERS, GUILD_MEMBER_ROLE, GUILD_INVITES, @@ -319,6 +324,7 @@ export { CUSTOM_EMOJI, GUILD_ICON, GUILD_SPLASH, + CLIENT_USER, GUILD_DISCOVERY_SPLASH, GUILD_BANNER, DEFAULT_USER_AVATAR, diff --git a/src/types/gateway.ts b/src/types/gateway.ts index 49d038e..224c3d1 100644 --- a/src/types/gateway.ts +++ b/src/types/gateway.ts @@ -1,5 +1,6 @@ // https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway // https://discord.com/developers/docs/topics/gateway#commands-and-events-gateway-events +import { StatusType } from "../../mod.ts" import { EmojiPayload } from './emoji.ts' import { MemberPayload } from './guild.ts' import { ActivityPayload } from './presence.ts' @@ -9,7 +10,7 @@ import { UserPayload } from './user.ts' /** * Gateway OPcodes from Discord docs. */ -enum GatewayOpcodes { // 문서를 확인해본 결과 Opcode 5번은 비어있다. - UnderC - +export enum GatewayOpcodes { // 문서를 확인해본 결과 Opcode 5번은 비어있다. - UnderC - DISPATCH = 0, HEARTBEAT = 1, IDENTIFY = 2, @@ -26,7 +27,7 @@ enum GatewayOpcodes { // 문서를 확인해본 결과 Opcode 5번은 비어있 /** * Gateway Close Codes from Discord docs. */ -enum GatewayCloseCodes { +export enum GatewayCloseCodes { UNKNOWN_ERROR = 4000, UNKNOWN_OPCODE = 4001, DECODE_ERROR = 4002, @@ -43,7 +44,7 @@ enum GatewayCloseCodes { DISALLOWED_INTENTS = 4014 } -enum GatewayIntents { +export enum GatewayIntents { GUILDS = 1 << 0, GUILD_MEMBERS = 1 << 1, GUILD_BANS = 1 << 2, @@ -61,7 +62,7 @@ enum GatewayIntents { DIRECT_MESSAGE_TYPING = 1 << 13 } -enum GatewayEvents { +export enum GatewayEvents { Ready = 'READY', Resumed = 'RESUMED', Reconnect = 'RECONNECT', @@ -111,7 +112,7 @@ export interface IdentityPayload { intents: number } -enum UpdateStatus { +export enum UpdateStatus { online = 'online', dnd = 'dnd', afk = 'idle', @@ -291,7 +292,7 @@ export interface MessageReactionRemoveAllPayload { export interface PresenceUpdatePayload { user: UserPayload guild_id: string - status: string + status: StatusType activities: ActivityPayload[] client_status: UpdateStatus[] } @@ -313,13 +314,4 @@ export interface VoiceServerUpdatePayload { export interface WebhooksUpdatePayload { guild_id: string channel_id: string -} - -// https://discord.com/developers/docs/topics/gateway#typing-start-typing-start-event-fields -export { - GatewayCloseCodes, - GatewayOpcodes, - GatewayIntents, - GatewayEvents, - UpdateStatus -} +} \ No newline at end of file diff --git a/src/types/guild.ts b/src/types/guild.ts index ea73d52..d8438d6 100644 --- a/src/types/guild.ts +++ b/src/types/guild.ts @@ -1,6 +1,6 @@ import { ChannelPayload } from './channel.ts' import { EmojiPayload } from './emoji.ts' -import { PresenceUpdatePayload } from './presence.ts' +import { PresenceUpdatePayload } from './gateway.ts' import { RolePayload } from './role.ts' import { UserPayload } from './user.ts' import { VoiceStatePayload } from './voice.ts' @@ -63,23 +63,23 @@ export interface MemberPayload { mute: boolean } -enum MessageNotification { +export enum MessageNotification { ALL_MESSAGES = 0, ONLY_MENTIONS = 1 } -enum ContentFilter { +export enum ContentFilter { DISABLED = 0, MEMBERS_WITHOUT_ROLES = 1, ALL_MEMBERS = 3 } -enum MFA { +export enum MFA { NONE = 0, ELEVATED = 1 } -enum Verification { +export enum Verification { NONE = 0, LOW = 1, MEDIUM = 2, @@ -87,14 +87,14 @@ enum Verification { VERY_HIGH = 4 } -enum PremiumTier { +export enum PremiumTier { NONE = 0, TIER_1 = 1, TIER_2 = 2, TIER_3 = 3 } -enum SystemChannelFlags { +export enum SystemChannelFlags { SUPPRESS_JOIN_NOTIFICATIONS = 1 << 0, SUPPRESS_PREMIUM_SUBSCRIPTIONS = 1 << 1 } diff --git a/src/types/presence.ts b/src/types/presence.ts index e304012..c127856 100644 --- a/src/types/presence.ts +++ b/src/types/presence.ts @@ -50,13 +50,11 @@ export interface ActivitySecrets { match?: string } -enum ActivityFlags { +export enum ActivityFlags { INSTANCE = 1 << 0, JOIN = 1 << 1, SPECTATE = 1 << 2, JOIN_REQUEST = 1 << 3, SYNC = 1 << 4, PLAY = 1 << 5 -} - -export { ActivityFlags } +} \ No newline at end of file diff --git a/src/types/voice.ts b/src/types/voice.ts index 706d212..c6b3143 100644 --- a/src/types/voice.ts +++ b/src/types/voice.ts @@ -1,7 +1,7 @@ // https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice import { MemberPayload } from './guild.ts' -enum VoiceOpcodes { // VoiceOpcodes 추가 - UnderC - +export enum VoiceOpcodes { // VoiceOpcodes 추가 - UnderC - IDENTIFY = 0, SELECT_PROTOCOL = 1, READY = 2, @@ -15,7 +15,7 @@ enum VoiceOpcodes { // VoiceOpcodes 추가 - UnderC - CLIENT_DISCONNECT = 13 } -enum VoiceCloseCodes { +export enum VoiceCloseCodes { UNKNOWN_OPCODE = 4001, NOT_AUTHENTICATED = 4003, AUTHENTICATION_FAILED = 4004, diff --git a/src/utils/collection.ts b/src/utils/collection.ts index 728fcea..67bc83e 100644 --- a/src/utils/collection.ts +++ b/src/utils/collection.ts @@ -1,15 +1,9 @@ export class Collection extends Map { - maxSize?: number; - - set(key: K, value: V) { - if (this.maxSize || this.maxSize === 0) { - if (this.size >= this.maxSize) return this - } - + set(key: K, value: V): this { return super.set(key, value) } - array() { + array(): V[] { return [...this.values()] } @@ -21,53 +15,49 @@ export class Collection extends Map { return [...this.values()][this.size - 1] } - random() { + random(): V { const arr = [...this.values()] return arr[Math.floor(Math.random() * arr.length)] } - find(callback: (value: V, key: K) => boolean) { + find(callback: (value: V, key: K) => boolean): V | undefined { for (const key of this.keys()) { - const value = this.get(key)! + const value = this.get(key) as V + // eslint-disable-next-line standard/no-callback-literal if (callback(value, key)) return value } - // If nothing matched - } - filter(callback: (value: V, key: K) => boolean) { + filter(callback: (value: V, key: K) => boolean): Collection { const relevant = new Collection() this.forEach((value, key) => { if (callback(value, key)) relevant.set(key, value) - }); - - return relevant; + }) + return relevant } - map(callback: (value: V, key: K) => T) { + map(callback: (value: V, key: K) => T): T[] { const results = [] for (const key of this.keys()) { - const value = this.get(key)! + const value = this.get(key) as V results.push(callback(value, key)) } return results } - some(callback: (value: V, key: K) => boolean) { + some(callback: (value: V, key: K) => boolean): boolean { for (const key of this.keys()) { - const value = this.get(key)! + const value = this.get(key) as V if (callback(value, key)) return true } - return false } - every(callback: (value: V, key: K) => boolean) { + every(callback: (value: V, key: K) => boolean): boolean { for (const key of this.keys()) { - const value = this.get(key)! + const value = this.get(key) as V if (!callback(value, key)) return false } - return true } @@ -75,21 +65,21 @@ export class Collection extends Map { callback: (accumulator: T, value: V, key: K) => T, initialValue?: T, ): T { - let accumulator: T = initialValue! + let accumulator: T = initialValue as T for (const key of this.keys()) { - const value = this.get(key)! + const value = this.get(key) as V accumulator = callback(accumulator, value, key) } return accumulator } - static fromObject(object: { [key: string]: V }) { + static fromObject(object: { [key: string]: V }): Collection { return new Collection(Object.entries(object)) } - toObject() { - return Object.entries(this) + toObject(): { [name: string]: V } { + return Object.fromEntries(this) } } \ No newline at end of file diff --git a/src/utils/delay.ts b/src/utils/delay.ts index d0c5de7..6fcb1ec 100644 --- a/src/utils/delay.ts +++ b/src/utils/delay.ts @@ -1,3 +1,3 @@ -export const delay = (ms: number) => new Promise((resolve, reject) => { +export const delay = async (ms: number): Promise => await new Promise((resolve, reject) => { setTimeout(() => resolve(true), ms); }); \ No newline at end of file diff --git a/src/utils/getChannelByType.ts b/src/utils/getChannelByType.ts index 8b0f9f1..9a8436d 100644 --- a/src/utils/getChannelByType.ts +++ b/src/utils/getChannelByType.ts @@ -14,7 +14,9 @@ import { GroupDMChannel } from '../structures/groupChannel.ts' import { CategoryChannel } from '../structures/guildCategoryChannel.ts' import { NewsChannel } from '../structures/guildNewsChannel.ts' import { VoiceChannel } from '../structures/guildVoiceChannel.ts' -import { TextChannel } from '../structures/textChannel.ts' +import { Guild } from "../structures/guild.ts" +import { GuildTextChannel } from "../structures/guildTextChannel.ts" +import { TextChannel } from "../structures/textChannel.ts" const getChannelByType = ( client: Client, @@ -25,7 +27,8 @@ const getChannelByType = ( | GuildVoiceChannelPayload | DMChannelPayload | GroupDMChannelPayload - | ChannelPayload + | ChannelPayload, + guild?: Guild ): | CategoryChannel | NewsChannel @@ -36,13 +39,17 @@ const getChannelByType = ( | undefined => { switch (data.type) { case ChannelTypes.GUILD_CATEGORY: - return new CategoryChannel(client, data as GuildChannelCategoryPayload) + if (guild === undefined) throw new Error("No Guild was provided to construct Channel") + return new CategoryChannel(client, data as GuildChannelCategoryPayload, guild) case ChannelTypes.GUILD_NEWS: - return new NewsChannel(client, data as GuildNewsChannelPayload) + if (guild === undefined) throw new Error("No Guild was provided to construct Channel") + return new NewsChannel(client, data as GuildNewsChannelPayload, guild) case ChannelTypes.GUILD_TEXT: - return new TextChannel(client, data as GuildTextChannelPayload) + if (guild === undefined) throw new Error("No Guild was provided to construct Channel") + return new GuildTextChannel(client, data as GuildTextChannelPayload, guild) case ChannelTypes.GUILD_VOICE: - return new VoiceChannel(client, data as GuildVoiceChannelPayload) + if (guild === undefined) throw new Error("No Guild was provided to construct Channel") + return new VoiceChannel(client, data as GuildVoiceChannelPayload, guild) case ChannelTypes.DM: return new DMChannel(client, data as DMChannelPayload) case ChannelTypes.GROUP_DM: