diff --git a/README.md b/README.md index 8efc3ca..891df9b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,22 @@ # discord-deno -![banner](images/discord-deno.png) +![banner](banner.png) [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) -**An easy to use Discord API Library for Deno** +**An easy to use Discord API Library for Deno.** +* Lightweight and easy to use. +* Built-in Command Framework, + * Easily build Commands on the fly. + * Compltely Customizable. + * Complete Object-Oriented approach. +* 100% Discord API Coverage. +* Customizable caching. + * Built in support for Redis. + * Write Custom Cache Adapters. +* Complete TypeScript support. + +Note: Library is yet under development and not completely usable. You're still always welcome to use, but there may be breaking changes. ## Table of Contents @@ -15,25 +27,70 @@ - [License](#license) ## Usage +Right now, the package is not published anywhere, as its not completely usable. +You can import it from this Raw GitHub URL: https://raw.githubusercontent.com/discord-deno/discord.deno/main/mod.ts +For a quick example, run this: +```bash +deno run --allow-net https://raw.githubusercontent.com/discord-deno/discord.deno/main/examples/ping.ts +``` +And input your bot's token and Intents. + +Here is a small example of how to use discord.deno, ```ts -import { Client } from 'https://deno.land/x/discord-deno/models/client.ts' -import { Message } from 'https://deno.land/x/discord-deno/structures/message.ts' +import { Client, Message, Intents } from 'https://raw.githubusercontent.com/discord-deno/discord.deno/main/mod.ts' -const bot = new Client() +const client = new Client() -bot.on('messageCreate', (msg: Message): void => { +// Listen for event when client is ready (Identified through gateway / Resumed) +client.on('ready', () => { + console.log(`Ready! User: ${client.user?.tag}`) +}) + +// Listen for event whenever a Message is sent +client.on('messageCreate', (msg: Message): void => { if (msg.content === '!ping') { - msg.channel.send(`Pong! ping: ${bot.ping}`) + msg.channel.send(`Pong! WS Ping: ${client.ping}`) } }) -bot.connect(TOKEN, [GatewayIntents.GUILD_MESSAGES]) +// Connect to gateway +// Replace with your bot's token and intents (Intents.All, Intents.Presence, Intents.GuildMembers) +client.connect('super secret token comes here', Intents.All) +``` + +Or with CommandClient! +```ts +import { CommandClient, Command, CommandContext, Message, Intents } from 'https://raw.githubusercontent.com/discord-deno/discord.deno/main/mod.ts' + +const client = new CommandClient({ + prefix: '!' +}) + +// Listen for event when client is ready (Identified through gateway / Resumed) +client.on('ready', () => { + console.log(`Ready! User: ${client.user?.tag}`) +}) + +// Create a new Command +class PingCommand extends Command { + name = "ping" + + execute(ctx: CommandContext) { + ctx.message.reply(`pong! Ping: ${ctx.client.ping}ms`) + } +} + +client.commands.add(PingCommand) + +// Connect to gateway +// Replace with your bot's token and intents (Intents.All, Intents.Presence, Intents.GuildMembers) +client.connect('super secret token comes here', Intents.All) ``` ## Docs -Not made yet +Not made yet. ## Maintainer diff --git a/examples/ping.ts b/examples/ping.ts new file mode 100644 index 0000000..3abf318 --- /dev/null +++ b/examples/ping.ts @@ -0,0 +1,41 @@ +import { Client, Message, Intents } from '../mod.ts' + +const client = new Client(); + +client.on("ready", () => { + console.log(`Logged in as ${client.user?.tag}!`); +}); + +client.on("messageCreate", (msg: Message) => { + if (msg.content === "!ping") { + console.log("Command Used: Ping"); + msg.reply("pong!"); + } +}); + +console.log("discord.deno - ping example"); + +const token = prompt("Input Bot Token:"); +if (token === null) { + console.log("No token provided"); + Deno.exit(); +} + +const intents = prompt("Input Intents (0 = All, 1 = Presence, 2 = Server Members, 3 = None):"); +if (intents === null || !["0", "1", "2", "3"].includes(intents)) { + console.log("No intents provided"); + Deno.exit(); +} + +let ints; +if (intents === "0") { + ints = Intents.All; +} else if (intents === "1") { + ints = Intents.Presence; +} else if (intents === "2") { + ints = Intents.GuildMembers; +} else { + ints = Intents.None; +} + +client.connect(token, ints); \ No newline at end of file diff --git a/mod.ts b/mod.ts index f38453a..f9881e5 100644 --- a/mod.ts +++ b/mod.ts @@ -3,6 +3,8 @@ 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/models/command.ts' +export * from './src/models/commandClient.ts' export * from './src/managers/base.ts' export * from './src/managers/baseChild.ts' export * from './src/managers/channels.ts' @@ -52,4 +54,5 @@ 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 +export * from './src/utils/collection.ts' +export * from './src/utils/intents.ts' \ No newline at end of file diff --git a/src/gateway/handlers/channelUpdate.ts b/src/gateway/handlers/channelUpdate.ts index d77777c..d6e633a 100644 --- a/src/gateway/handlers/channelUpdate.ts +++ b/src/gateway/handlers/channelUpdate.ts @@ -13,7 +13,7 @@ export const channelUpdate: GatewayEventHandler = async ( if (oldChannel !== undefined) { await gateway.client.channels.set(d.id, d) let guild: undefined | Guild; - if('guild_id' in d) { + if ('guild_id' in d) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion guild = await gateway.client.guilds.get((d as GuildChannelPayload).guild_id) as Guild | undefined } diff --git a/src/gateway/handlers/messageCreate.ts b/src/gateway/handlers/messageCreate.ts index 5e485ac..189e29c 100644 --- a/src/gateway/handlers/messageCreate.ts +++ b/src/gateway/handlers/messageCreate.ts @@ -17,7 +17,7 @@ export const messageCreate: GatewayEventHandler = async ( 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) { + if (d.guild_id !== undefined) { guild = await gateway.client.guilds.get(d.guild_id) } const mentions = new MessageMentions() diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 7de2bac..483eb11 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -1,4 +1,4 @@ -import { unzlib } from 'https://deno.land/x/denoflate/mod.ts' +import { unzlib } from 'https://deno.land/x/denoflate@1.1/mod.ts' import { Client } from '../models/client.ts' import { DISCORD_GATEWAY_URL, @@ -76,21 +76,10 @@ class Gateway { this.debug( `Received HELLO. Heartbeat Interval: ${this.heartbeatInterval}` ) - this.heartbeatIntervalID = setInterval(() => { - if (this.heartbeatServerResponded) { - this.heartbeatServerResponded = false - } else { - clearInterval(this.heartbeatIntervalID) - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.reconnect() - return - } - this.send({ - op: GatewayOpcodes.HEARTBEAT, - d: this.sequenceID ?? null - }) - this.lastPingTimestamp = Date.now() + this.sendHeartbeat() + this.heartbeatIntervalID = setInterval(() => { + this.heartbeat() }, this.heartbeatInterval) if (!this.initialized) { @@ -151,7 +140,7 @@ class Gateway { } } - private onclose (event: CloseEvent): void { + private onclose(event: CloseEvent): void { this.debug(`Connection Closed with code: ${event.code}`) if (event.code === GatewayCloseCodes.UNKNOWN_ERROR) { @@ -200,28 +189,31 @@ class Gateway { console.log(eventError) } - private async sendIdentify (forceNewSession?: boolean): Promise { - this.debug('Fetching /gateway/bot...') - const info = await this.client.rest.get(GATEWAY_BOT()) - if (info.session_start_limit.remaining === 0) - throw new Error( - `Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms` + private async sendIdentify(forceNewSession?: boolean): Promise { + if (this.client.bot === true) { + this.debug('Fetching /gateway/bot...') + const info = await this.client.rest.get(GATEWAY_BOT()) + if (info.session_start_limit.remaining === 0) + throw new Error( + `Session Limit Reached. Retry After ${info.session_start_limit.reset_after}ms` + ) + this.debug(`Recommended Shards: ${info.shards}`) + this.debug('=== Session Limit Info ===') + this.debug( + `Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}` ) - this.debug(`Recommended Shards: ${info.shards}`) - this.debug('=== Session Limit Info ===') - this.debug( - `Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}` - ) - this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`) - if (forceNewSession === undefined || !forceNewSession) { - const sessionIDCached = await this.cache.get('session_id') - if (sessionIDCached !== undefined) { - this.debug(`Found Cached SessionID: ${sessionIDCached}`) - this.sessionID = sessionIDCached - return await this.sendResume() + this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`) + if (forceNewSession === undefined || !forceNewSession) { + const sessionIDCached = await this.cache.get('session_id') + if (sessionIDCached !== undefined) { + this.debug(`Found Cached SessionID: ${sessionIDCached}`) + this.sessionID = sessionIDCached + return await this.sendResume() + } } } - this.send({ + + const payload: any = { op: GatewayOpcodes.IDENTIFY, d: { token: this.token, @@ -238,7 +230,24 @@ class Gateway { ), presence: this.client.presence.create() } - }) + } + + 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 + payload.d.presence = null + payload.d.properties = { + $os: "Windows", + $browser: "Firefox", + $device: "" + } + + this.debug("Warn: Support for selfbots is incomplete") + } + + this.send(payload) } private async sendResume(): Promise { @@ -259,11 +268,11 @@ class Gateway { this.send(resumePayload) } - debug (msg: string): void { + debug(msg: string): void { this.client.debug('Gateway', msg) } - async reconnect (forceNew?: boolean): Promise { + async reconnect(forceNew?: boolean): Promise { clearInterval(this.heartbeatIntervalID) if (forceNew === undefined || !forceNew) await this.cache.delete('session_id') @@ -305,6 +314,29 @@ class Gateway { d: data }) } + + sendHeartbeat(): void { + const payload = { + op: GatewayOpcodes.HEARTBEAT, + d: this.sequenceID ?? null + }; + + this.send(payload) + this.lastPingTimestamp = Date.now() + } + + heartbeat(): void { + if (this.heartbeatServerResponded) { + this.heartbeatServerResponded = false + } else { + clearInterval(this.heartbeatIntervalID) + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.reconnect() + return + } + + this.sendHeartbeat() + } } export type GatewayEventHandler = (gateway: Gateway, d: any) => void diff --git a/src/models/cacheAdapter.ts b/src/models/cacheAdapter.ts index 6355622..406d2a4 100644 --- a/src/models/cacheAdapter.ts +++ b/src/models/cacheAdapter.ts @@ -1,5 +1,4 @@ import { Collection } from '../utils/collection.ts' -import { Client } from './client.ts' import { connect, Redis, @@ -7,7 +6,6 @@ import { } from 'https://denopkg.com/keroxp/deno-redis/mod.ts' export interface ICacheAdapter { - client: Client get: (cacheName: string, key: string) => Promise | any set: (cacheName: string, key: string, value: any) => Promise | any delete: (cacheName: string, key: string) => Promise | boolean @@ -16,15 +14,10 @@ export interface ICacheAdapter { } export class DefaultCacheAdapter implements ICacheAdapter { - client: Client data: { [name: string]: Collection } = {} - constructor (client: Client) { - this.client = client - } - async get (cacheName: string, key: string): Promise { const cache = this.data[cacheName] if (cache === undefined) return @@ -59,13 +52,11 @@ export class DefaultCacheAdapter implements ICacheAdapter { } export class RedisCacheAdapter implements ICacheAdapter { - client: Client _redis: Promise redis?: Redis ready: boolean = false - constructor (client: Client, options: RedisConnectOptions) { - this.client = client + constructor (options: RedisConnectOptions) { this._redis = connect(options) this._redis.then( redis => { diff --git a/src/models/client.ts b/src/models/client.ts index 056b82e..9f3f817 100644 --- a/src/models/client.ts +++ b/src/models/client.ts @@ -18,6 +18,8 @@ export interface ClientOptions { cache?: ICacheAdapter, forceNewSession?: boolean, presence?: ClientPresence | ClientActivity | ActivityGame + bot?: boolean + canary?: boolean } /** @@ -29,7 +31,7 @@ export class Client extends EventEmitter { user?: User ping = 0 token?: string - cache: ICacheAdapter = new DefaultCacheAdapter(this) + cache: ICacheAdapter = new DefaultCacheAdapter() intents?: GatewayIntents[] forceNewSession?: boolean users: UserManager = new UserManager(this) @@ -37,6 +39,8 @@ export class Client extends EventEmitter { channels: ChannelsManager = new ChannelsManager(this) messages: MessagesManager = new MessagesManager(this) emojis: EmojisManager = new EmojisManager(this) + bot: boolean = true + canary: boolean = false presence: ClientPresence = new ClientPresence() @@ -47,6 +51,8 @@ export class Client extends EventEmitter { 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) + if (options.bot === false) this.bot = false + if (options.canary === true) this.canary = true } setAdapter (adapter: ICacheAdapter): Client { diff --git a/src/models/command.ts b/src/models/command.ts new file mode 100644 index 0000000..3e60a64 --- /dev/null +++ b/src/models/command.ts @@ -0,0 +1,126 @@ +import { Message } from "../structures/message.ts" +import { TextChannel } from "../structures/textChannel.ts" +import { User } from "../structures/user.ts" +import { Collection } from "../utils/collection.ts" +import { CommandClient } from "./commandClient.ts" + +export interface CommandContext { + /** The Client object */ + client: CommandClient + /** Message which was parsed for Command */ + message: Message + /** The Author of the Message */ + author: User + /** The Channel in which Command was used */ + channel: TextChannel + /** Prefix which was used */ + prefix: string + /** Oject of Command which was used */ + command: Command + /** Name of Command which was used */ + name: string + /** Array of Arguments used with Command */ + args: string[] + /** Complete Raw String of Arguments */ + argString: string +} + +export class Command { + /** Name of the Command */ + name: string = "" + /** Description of the Command */ + description?: string + /** Array of Aliases of Command, or only string */ + aliases?: string | string[] + /** Usage of Command, only Argument Names */ + usage?: string | string[] + /** Usage Example of Command, only Arguments (without Prefix and Name) */ + examples?: string | string[] + /** Does the Command take Arguments? Maybe number of required arguments? */ + args?: number | boolean + /** Permission(s) required for using Command */ + permissions?: string | string[] + /** Whether the Command can only be used in Guild (if allowed in DMs) */ + guildOnly?: boolean + /** Whether the Command can only be used in Bot's DMs (if allowed) */ + dmOnly?: boolean + /** Whether the Command can only be used by Bot Owners */ + ownerOnly?: boolean + + execute(ctx?: CommandContext): any { } +} + +export class CommandsManager { + client: CommandClient + list: Collection = new Collection() + + constructor(client: CommandClient) { + this.client = client + } + + /** Find a Command by name/alias */ + find(search: string): Command | undefined { + if (this.client.caseSensitive === false) search = search.toLowerCase() + return this.list.find((cmd: Command): boolean => { + const name = this.client.caseSensitive === true ? cmd.name : cmd.name.toLowerCase() + if (name === search) return true + else if (cmd.aliases !== undefined) { + let aliases: string[] + if (typeof cmd.aliases === "string") aliases = [cmd.aliases] + else aliases = cmd.aliases + if (this.client.caseSensitive === false) aliases = aliases.map(e => e.toLowerCase()) + return aliases.includes(search) + } else return false + }) + } + + /** Check whether a Command exists or not */ + exists(search: Command | string): boolean { + let exists = false + if (typeof search === "string") return this.find(search) !== undefined + else { + exists = this.find(search.name) !== undefined + if (search.aliases !== undefined) { + const aliases: string[] = typeof search.aliases === "string" ? [search.aliases] : search.aliases + exists = aliases.map(alias => this.find(alias) !== undefined).find(e => e) ?? false + } + return exists + } + } + + /** Add a Command */ + 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 + this.list.set(cmd.name, cmd) + return true + } + + /** Delete a Command */ + 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) + } +} + +export interface ParsedCommand { + name: string + args: string[] + argString: string +} + +export const parseCommand = (client: CommandClient, msg: Message, prefix: string): ParsedCommand => { + let content = msg.content.slice(prefix.length) + if (client.spacesAfterPrefix === true) content = content.trim() + const args = content.split(client.betterArgs === true ? /[\S\s]*/ : / +/) + const name = args.shift() as string + const argString = content.slice(name.length).trim() + + return { + name, + args, + argString + } +} \ No newline at end of file diff --git a/src/models/commandClient.ts b/src/models/commandClient.ts new file mode 100644 index 0000000..458fe2f --- /dev/null +++ b/src/models/commandClient.ts @@ -0,0 +1,161 @@ +import { Embed, Message } from '../../mod.ts' +import { Client, ClientOptions } from './client.ts' +import { CommandContext, CommandsManager, parseCommand } from './command.ts' + +type PrefixReturnType = string | string[] | Promise + +export interface CommandClientOptions extends ClientOptions { + prefix: string | string[] + mentionPrefix?: boolean + getGuildPrefix?: (guildID: string) => PrefixReturnType + getUserPrefix?: (userID: string) => PrefixReturnType + spacesAfterPrefix?: boolean + betterArgs?: boolean + owners?: string[] + allowBots?: boolean + allowDMs?: boolean + caseSensitive?: boolean +} + +type CommandText = string | Embed + +export interface CommandTexts { + GUILD_ONLY?: CommandText + OWNER_ONLY?: CommandText + DMS_ONLY?: CommandText + ERROR?: CommandText +} + +export const DefaultCommandTexts: CommandTexts = { + GUILD_ONLY: 'This command can only be used in a Server!', + OWNER_ONLY: 'This command can only be used by Bot Owners!', + DMS_ONLY: 'This command can only be used in Bot\'s DMs!', + ERROR: 'An error occured while executing command!' +} + +interface Replaces { + [name: string]: string +} + +export const massReplace = (text: string, replaces: Replaces): string => { + Object.entries(replaces).forEach(replace => { + text = text.replace(new RegExp(`{{${replace[0]}}}`, 'g'), replace[1]) + }) + return text +} + +export class CommandClient extends Client { + prefix: string | string[] + mentionPrefix: boolean + getGuildPrefix: (guildID: string) => PrefixReturnType + getUserPrefix: (userID: string) => PrefixReturnType + spacesAfterPrefix: boolean + betterArgs: boolean + owners: string[] + allowBots: boolean + allowDMs: boolean + caseSensitive: boolean + commands: CommandsManager = new CommandsManager(this) + texts: CommandTexts = DefaultCommandTexts + + constructor(options: CommandClientOptions) { + super(options) + this.prefix = options.prefix + this.mentionPrefix = options.mentionPrefix === undefined ? false : options.mentionPrefix + this.getGuildPrefix = options.getGuildPrefix === undefined ? (id: string) => this.prefix : options.getGuildPrefix + this.getUserPrefix = options.getUserPrefix === undefined ? (id: string) => this.prefix : options.getUserPrefix + this.spacesAfterPrefix = options.spacesAfterPrefix === undefined ? false : options.spacesAfterPrefix + this.betterArgs = options.betterArgs === undefined ? false : options.betterArgs + this.owners = options.owners === undefined ? [] : options.owners + this.allowBots = options.allowBots === undefined ? false : options.allowBots + this.allowDMs = options.allowDMs === undefined ? true : options.allowDMs + this.caseSensitive = options.caseSensitive === undefined ? false : options.caseSensitive + + this.on('messageCreate', async (msg: Message) => await this.processMessage(msg)) + } + + async processMessage(msg: Message): Promise { + if (!this.allowBots && msg.author.bot === 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 + } else { + let userPrefix = this.getUserPrefix(msg.author.id) + if (userPrefix instanceof Promise) userPrefix = await userPrefix + prefix = userPrefix + } + + if (typeof prefix === 'string') { + if (msg.content.startsWith(prefix) === false) return + } else { + const usedPrefix = prefix.find(v => msg.content.startsWith(v)) + if (usedPrefix === undefined) return + else prefix = usedPrefix + } + + const parsed = parseCommand(this, msg, prefix) + const command = this.commands.find(parsed.name) + + if (command === undefined) return + + const baseReplaces: Replaces = { + command: command.name, + nameUsed: parsed.name, + prefix, + username: msg.author.username, + tag: msg.author.tag, + mention: msg.author.mention, + id: msg.author.id + } + + if (command.guildOnly === true && msg.guild === undefined) { + if (this.texts.GUILD_ONLY !== undefined) return this.sendProcessedText(msg, this.texts.GUILD_ONLY, baseReplaces) + return + } + if (command.dmOnly === true && msg.guild !== undefined) { + if (this.texts.DMS_ONLY !== undefined) return this.sendProcessedText(msg, this.texts.DMS_ONLY, baseReplaces) + return + } + if (command.ownerOnly === true && !this.owners.includes(msg.author.id)) { + if (this.texts.OWNER_ONLY !== undefined) return this.sendProcessedText(msg, this.texts.OWNER_ONLY, baseReplaces) + return + } + + const ctx: CommandContext = { + client: this, + name: parsed.name, + prefix, + args: parsed.args, + argString: parsed.argString, + message: msg, + author: msg.author, + command, + channel: msg.channel + } + + try { + this.emit('commandUsed', { context: ctx }) + command.execute(ctx) + } catch (e) { + if (this.texts.ERROR !== undefined) return this.sendProcessedText(msg, this.texts.ERROR, Object.assign(baseReplaces, { error: e.message })) + this.emit('commandError', { command, parsed, error: e }) + } + } + + sendProcessedText(msg: Message, text: CommandText, replaces: Replaces): any { + if (typeof text === "string") { + text = massReplace(text, replaces) + return msg.channel.send(text) + } else { + if (text.description !== undefined) text.description = massReplace(text.description, replaces) + if (text.title !== undefined) text.description = massReplace(text.title, replaces) + if (text.author?.name !== undefined) text.description = massReplace(text.author.name, replaces) + if (text.footer?.text !== undefined) text.description = massReplace(text.footer.text, replaces) + return msg.channel.send(text) + } + } +} \ No newline at end of file diff --git a/src/models/rest.ts b/src/models/rest.ts index 36f34b4..8245d9a 100644 --- a/src/models/rest.ts +++ b/src/models/rest.ts @@ -1,6 +1,7 @@ import { delay } from '../utils/index.ts' import * as baseEndpoints from '../consts/urlsAndVersions.ts' import { Client } from './client.ts' +import { getBuildInfo } from "../utils/buildInfo.ts" export enum HttpResponseCode { Ok = 200, @@ -175,11 +176,29 @@ export class RESTManager { headers['Content-Type'] = 'application/json' } - return { + const data: { [name: string]: any } = { headers, body: body?.file ?? JSON.stringify(body), method: method.toUpperCase() } + + if (this.client.bot === false) { + // This is a selfbot. Use requests similar to Discord Client + data.headers.authorization = this.client.token as string + data.headers['accept-language'] = 'en-US' + data.headers.accept = '*/*' + data.headers['sec-fetch-dest'] = 'empty' + data.headers['sec-fetch-mode'] = 'cors' + data.headers['sec-fetch-site'] = 'same-origin' + data.headers['x-super-properties'] = btoa(JSON.stringify(getBuildInfo(this.client))) + delete data.headers['User-Agent'] + delete data.headers.Authorization + headers.credentials = 'include' + headers.mode = 'cors' + headers.referrerPolicy = 'no-referrer-when-downgrade' + } + + return data } async checkRatelimits (url: string): Promise { @@ -230,9 +249,14 @@ export class RESTManager { ) .join('&') : '' - const urlToUse = + let urlToUse = method === 'get' && query !== '' ? `${url}?${query}` : url + if (this.client.canary === true) { + const split = urlToUse.split('//') + urlToUse = split[0] + '//canary.' + split[1] + } + const requestData = this.createRequestBody(body, method) const response = await fetch( @@ -304,7 +328,7 @@ 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.") + if (status === HttpResponseCode.Unauthorized) throw new Error("Request was not successful. Invalid Token.") switch (status) { case HttpResponseCode.BadRequest: diff --git a/src/structures/message.ts b/src/structures/message.ts index cccaf3c..0890e02 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -88,7 +88,7 @@ export class Message extends Base { this.flags = data.flags this.channel = channel // TODO: Cache in Gateway Event Code - // if(!noSave) this.client.messages.set(this.id, data) + // if (!noSave) this.client.messages.set(this.id, data) } protected readFromData (data: MessagePayload): void { diff --git a/src/structures/messageMentions.ts b/src/structures/messageMentions.ts index 4888838..6ef2a00 100644 --- a/src/structures/messageMentions.ts +++ b/src/structures/messageMentions.ts @@ -1,3 +1,3 @@ export class MessageMentions { str: string = "str" -} \ No newline at end of file +} diff --git a/src/structures/textChannel.ts b/src/structures/textChannel.ts index 3955fa2..daa8685 100644 --- a/src/structures/textChannel.ts +++ b/src/structures/textChannel.ts @@ -2,9 +2,12 @@ import { Client } from '../models/client.ts' import { MessageOption, 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 { Message } from './message.ts' import { MessageMentions } from './messageMentions.ts' +type AllMessageOptions = MessageOption | Embed + export class TextChannel extends Channel { lastMessageID?: string lastPinTimestamp?: string @@ -23,10 +26,18 @@ export class TextChannel extends Channel { this.lastPinTimestamp = data.last_pin_timestamp ?? this.lastPinTimestamp } - async send (text?: string, option?: MessageOption): Promise { + async send (text?: string | AllMessageOptions, option?: AllMessageOptions): Promise { + if(typeof text === "object") { + option = text + text = undefined + } if (text === undefined && option === undefined) { throw new Error('Either text or option is necessary.') } + if(option instanceof Embed) option = { + embed: option + } + const resp = await this.client.rest.post(CHANNEL_MESSAGES(this.id), { content: text, embed: option?.embed, diff --git a/src/test/cmd.ts b/src/test/cmd.ts new file mode 100644 index 0000000..ceba966 --- /dev/null +++ b/src/test/cmd.ts @@ -0,0 +1,18 @@ +import { CommandClient, Intents } from '../../mod.ts'; +import PingCommand from "./cmds/ping.ts"; +import { TOKEN } from './config.ts' + +const client = new CommandClient({ + prefix: ["pls", "!"], + spacesAfterPrefix: true +}) + +client.on('debug', console.log) + +client.on('ready', () => { + console.log(`[Login] Logged in as ${client.user?.tag}!`) +}) + +client.commands.add(PingCommand) + +client.connect(TOKEN, Intents.All) \ No newline at end of file diff --git a/src/test/cmds/ping.ts b/src/test/cmds/ping.ts new file mode 100644 index 0000000..253142e --- /dev/null +++ b/src/test/cmds/ping.ts @@ -0,0 +1,11 @@ +import { Command } from "../../../mod.ts"; +import { CommandContext } from "../../models/command.ts"; + +export default class PingCommand extends Command { + name = "ping" + dmOnly = true + + execute(ctx: CommandContext): void { + ctx.message.reply(`pong! Latency: ${ctx.client.ping}ms`) + } +} \ No newline at end of file diff --git a/src/test/index.ts b/src/test/index.ts index dc925ec..4082eaa 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,90 +1,62 @@ -import { Client } from '../models/client.ts' -import { GatewayIntents } from '../types/gateway.ts' +import { Client, Intents, GuildTextChannel, Message, ClientPresence, Member, Role, GuildChannel, Embed, Guild } from '../../mod.ts'; import { TOKEN } from './config.ts' -import { Message } from "../structures/message.ts" -import { DefaultCacheAdapter } 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/guildChannels.ts" -import { TextChannel } from "../structures/textChannel.ts" -import { Embed } from "../structures/embed.ts" -import { Guild } from "../structures/guild.ts" -const bot = new Client({ +const client = new Client({ presence: new ClientPresence({ - activity: { - name: "Testing", + name: 'Pokémon Sword', type: 'COMPETING' - } }), + // bot: false, + // cache: new RedisCacheAdapter({ + // hostname: '127.0.0.1', + // port: 6379 + // }) // Defaults to in-memory Caching }) -bot.setAdapter(new DefaultCacheAdapter(bot)) - -bot.on('ready', () => { - console.log(`[Login] Logged in as ${bot.user?.tag}!`) - bot.setPresence({ - name: "Test After Ready", - type: 'COMPETING' - }) +client.on('ready', () => { + console.log(`[Login] Logged in as ${client.user?.tag}!`) }) -bot.on('debug', console.log) +client.on('debug', console.log) -bot.on('channelPinsUpdate', (before: TextChannel, after: TextChannel) => { +client.on('channelUpdate', (before: GuildTextChannel, after: GuildTextChannel) => { console.log(before.send('', { embed: new Embed({ - title: 'Test', - description: 'Test Embed' + title: 'Channel Update', + description: `Name Before: ${before.name}\nName After: ${after.name}` }) })) }) -bot.on('messageCreate', async (msg: Message) => { +client.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") { + console.log(`${msg.author.tag}: ${msg.content}`) + if (msg.content === '!ping') { + msg.reply(`Pong! Ping: ${client.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") { + } else if (msg.content === '!guilds') { const guilds = await msg.client.guilds.collection() - msg.channel.send("Guild List:\n" + (guilds.array().map((c: Guild, i: number) => { + msg.channel.send('Guild List:\n' + (guilds.array().map((c: Guild, i: number) => { return `${i + 1}. ${c.name} - ${c.memberCount} members` }).join("\n") as string)) - } else if (msg.content === "!roles") { + } 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") { + } 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) + msg.channel.send('Channels List:\n' + data) } }) -bot.connect(TOKEN, [ - GatewayIntents.GUILD_MEMBERS, - GatewayIntents.GUILD_PRESENCES, - GatewayIntents.GUILD_MESSAGES, - GatewayIntents.DIRECT_MESSAGES, - GatewayIntents.DIRECT_MESSAGE_REACTIONS, - GatewayIntents.DIRECT_MESSAGE_TYPING, - GatewayIntents.GUILDS, - GatewayIntents.GUILD_BANS, - GatewayIntents.GUILD_EMOJIS, - GatewayIntents.GUILD_INTEGRATIONS, - GatewayIntents.GUILD_INVITES, - GatewayIntents.GUILD_MESSAGE_REACTIONS, - GatewayIntents.GUILD_MESSAGE_TYPING, - GatewayIntents.GUILD_VOICE_STATES, - GatewayIntents.GUILD_WEBHOOKS -]) +client.connect(TOKEN, Intents.None) diff --git a/src/utils/buildInfo.ts b/src/utils/buildInfo.ts new file mode 100644 index 0000000..7d37a5f --- /dev/null +++ b/src/utils/buildInfo.ts @@ -0,0 +1,40 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Client } from "../models/client.ts"; + +export const getBuildInfo = (client: Client): { + os: string + os_version: string + browser: string + browser_version: string + browser_user_agent: string + client_build_number: number + client_event_source: null + release_channel: string +} => { + const os = 'Windows' + const os_version = '10' + let client_build_number = 71073 + const client_event_source = null + let release_channel = 'stable' + if (client.canary === true) { + release_channel = 'canary' + client_build_number = 71076 + } + const browser = 'Firefox' + const browser_version = '83.0' + const browser_user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 ' + browser + '/' + browser_version + // TODO: Use current OS properties, but also browser_user_agent accordingly + // if (Deno.build.os === 'darwin') os = 'MacOS' + // else if (Deno.build.os === 'linux') os = 'Ubuntu' + + return { + os, + os_version, + browser, + browser_version, + browser_user_agent, + client_build_number, + client_event_source, + release_channel, + } +}; \ No newline at end of file diff --git a/src/utils/intents.ts b/src/utils/intents.ts new file mode 100644 index 0000000..09f980f --- /dev/null +++ b/src/utils/intents.ts @@ -0,0 +1,72 @@ +import { GatewayIntents } from "../types/gateway.ts"; + +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class Intents { + static All: number[] = [ + GatewayIntents.GUILD_MEMBERS, + GatewayIntents.GUILD_PRESENCES, + GatewayIntents.GUILD_MESSAGES, + GatewayIntents.DIRECT_MESSAGES, + GatewayIntents.DIRECT_MESSAGE_REACTIONS, + GatewayIntents.DIRECT_MESSAGE_TYPING, + GatewayIntents.GUILDS, + GatewayIntents.GUILD_BANS, + GatewayIntents.GUILD_EMOJIS, + GatewayIntents.GUILD_INTEGRATIONS, + GatewayIntents.GUILD_INVITES, + GatewayIntents.GUILD_MESSAGE_REACTIONS, + GatewayIntents.GUILD_MESSAGE_TYPING, + GatewayIntents.GUILD_VOICE_STATES, + GatewayIntents.GUILD_WEBHOOKS + ]; + + static Presence: number[] = [ + GatewayIntents.GUILD_PRESENCES, + GatewayIntents.GUILD_MESSAGES, + GatewayIntents.DIRECT_MESSAGES, + GatewayIntents.DIRECT_MESSAGE_REACTIONS, + GatewayIntents.DIRECT_MESSAGE_TYPING, + GatewayIntents.GUILDS, + GatewayIntents.GUILD_BANS, + GatewayIntents.GUILD_EMOJIS, + GatewayIntents.GUILD_INTEGRATIONS, + GatewayIntents.GUILD_INVITES, + GatewayIntents.GUILD_MESSAGE_REACTIONS, + GatewayIntents.GUILD_MESSAGE_TYPING, + GatewayIntents.GUILD_VOICE_STATES, + GatewayIntents.GUILD_WEBHOOKS + ]; + + static GuildMembers: number[] = [ + GatewayIntents.GUILD_MEMBERS, + GatewayIntents.GUILD_MESSAGES, + GatewayIntents.DIRECT_MESSAGES, + GatewayIntents.DIRECT_MESSAGE_REACTIONS, + GatewayIntents.DIRECT_MESSAGE_TYPING, + GatewayIntents.GUILDS, + GatewayIntents.GUILD_BANS, + GatewayIntents.GUILD_EMOJIS, + GatewayIntents.GUILD_INTEGRATIONS, + GatewayIntents.GUILD_INVITES, + GatewayIntents.GUILD_MESSAGE_REACTIONS, + GatewayIntents.GUILD_MESSAGE_TYPING, + GatewayIntents.GUILD_VOICE_STATES, + GatewayIntents.GUILD_WEBHOOKS + ]; + + static None: number[] = [ + GatewayIntents.GUILD_MESSAGES, + GatewayIntents.DIRECT_MESSAGES, + GatewayIntents.DIRECT_MESSAGE_REACTIONS, + GatewayIntents.DIRECT_MESSAGE_TYPING, + GatewayIntents.GUILDS, + GatewayIntents.GUILD_BANS, + GatewayIntents.GUILD_EMOJIS, + GatewayIntents.GUILD_INTEGRATIONS, + GatewayIntents.GUILD_INVITES, + GatewayIntents.GUILD_MESSAGE_REACTIONS, + GatewayIntents.GUILD_MESSAGE_TYPING, + GatewayIntents.GUILD_VOICE_STATES, + GatewayIntents.GUILD_WEBHOOKS + ] +} \ No newline at end of file