diff --git a/README.md b/README.md index fb85fb9..0602170 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # discord-deno +![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.** @@ -36,16 +38,19 @@ import { Client, Message, Intents } from 'https://raw.githubusercontent.com/disc const client = new Client() +// 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! WS Ping: ${client.ping}`) } }) +// 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) ``` diff --git a/examples/ping.ts b/examples/ping.ts index 4f34419..7ed21ed 100644 --- a/examples/ping.ts +++ b/examples/ping.ts @@ -21,8 +21,8 @@ if(!token) { Deno.exit(); } -const intents = prompt("Input Intents (0 = All, 1 = Presence, 2 = Server Members):"); -if(!intents || !["0", "1", "2"].includes(intents)) { +const intents = prompt("Input Intents (0 = All, 1 = Presence, 2 = Server Members, 3 = None):"); +if(!intents || !["0", "1", "2", "3"].includes(intents)) { console.log("No intents provided"); Deno.exit(); } @@ -32,8 +32,10 @@ if(intents == "0") { ints = Intents.All; } else if(intents == "1") { ints = Intents.Presence; -} else { +} else if(intents == "2") { ints = Intents.GuildMembers; +} else { + ints = Intents.None; } client.connect(token, ints); \ No newline at end of file diff --git a/src/gateway/index.ts b/src/gateway/index.ts index a52264a..584ca8e 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, @@ -140,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) { @@ -189,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({ + + let payload: any = { op: GatewayOpcodes.IDENTIFY, d: { token: this.token, @@ -227,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 { @@ -248,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') @@ -315,7 +335,7 @@ class Gateway { return } - this.sendHeartbeat() + this.sendHeartbeat() } } diff --git a/src/models/client.ts b/src/models/client.ts index 573a7b7..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 } /** @@ -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/rest.ts b/src/models/rest.ts index 36f34b4..d931ac8 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 { + let 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) { + let split = urlToUse.split('//') + urlToUse = split[0] + '//canary.' + split[1] + } + const requestData = this.createRequestBody(body, method) const response = await fetch( diff --git a/src/test/index.ts b/src/test/index.ts index ff8d3a9..4082eaa 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,5 +1,4 @@ -import { Client, GuildTextChannel, Message, RedisCacheAdapter, ClientPresence, Member, Role, GuildChannel, TextChannel, Embed, Guild } from '../../mod.ts'; -import { Intents } from "../utils/intents.ts"; +import { Client, Intents, GuildTextChannel, Message, ClientPresence, Member, Role, GuildChannel, Embed, Guild } from '../../mod.ts'; import { TOKEN } from './config.ts' const client = new Client({ @@ -7,6 +6,7 @@ const client = new Client({ name: 'Pokémon Sword', type: 'COMPETING' }), + // bot: false, // cache: new RedisCacheAdapter({ // hostname: '127.0.0.1', // port: 6379 @@ -30,6 +30,7 @@ client.on('channelUpdate', (before: GuildTextChannel, after: GuildTextChannel) = client.on('messageCreate', async (msg: Message) => { if (msg.author.bot === true) return + console.log(`${msg.author.tag}: ${msg.content}`) if (msg.content === '!ping') { msg.reply(`Pong! Ping: ${client.ping}ms`) } else if (msg.content === '!members') { @@ -58,4 +59,4 @@ client.on('messageCreate', async (msg: Message) => { } }) -client.connect(TOKEN, Intents.All) +client.connect(TOKEN, Intents.None) diff --git a/src/utils/buildInfo.ts b/src/utils/buildInfo.ts new file mode 100644 index 0000000..29339ac --- /dev/null +++ b/src/utils/buildInfo.ts @@ -0,0 +1,30 @@ +import { Client } from "../models/client.ts"; + +export const getBuildInfo = (client: Client) => { + let os = 'Windows' + let os_version = '10' + let client_build_number = 71073 + let client_event_source = null + let release_channel = 'stable' + if (client.canary) { + release_channel = 'canary' + client_build_number = 71076 + } + let browser = 'Firefox' + let browser_version = '83.0' + let 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 index 844618a..95c070e 100644 --- a/src/utils/intents.ts +++ b/src/utils/intents.ts @@ -52,4 +52,20 @@ export class Intents { 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