diff --git a/packages/gateway/src/Shard.ts b/packages/gateway/src/Shard.ts index 3ae320f0f..555ce9f9e 100644 --- a/packages/gateway/src/Shard.ts +++ b/packages/gateway/src/Shard.ts @@ -496,6 +496,8 @@ export class Shard { // Reference: https://discord.com/developers/docs/topics/gateway#heartbeating const jitter = Math.ceil(this.heart.interval * (Math.random() || 0.5)) this.heart.timeoutId = setTimeout(() => { + if (!this.isOpen()) return; + // Using a direct socket.send call here because heartbeat requests are reserved by us. this.socket?.send( JSON.stringify({ @@ -509,6 +511,7 @@ export class Shard { // After the random heartbeat jitter we can start a normal interval. this.heart.intervalId = setInterval(async () => { + if (!this.isOpen()) return; // gateway.debug("GW DEBUG", `Running setInterval in heartbeat file. Shard: ${shardId}`); // gateway.debug("GW HEARTBEATING", { shardId, shard: currentShard }); diff --git a/packages/rest/src/manager.ts b/packages/rest/src/manager.ts index 5552e4e15..b0d21c685 100644 --- a/packages/rest/src/manager.ts +++ b/packages/rest/src/manager.ts @@ -1,5 +1,5 @@ import { InteractionResponseTypes } from '@discordeno/types' -import { camelize, delay, findFiles, getBotIdFromToken, logger, urlToBase64 } from '@discordeno/utils' +import { camelize, camelToSnakeCase, delay, findFiles, getBotIdFromToken, logger, urlToBase64 } from '@discordeno/utils' import { createInvalidRequestBucket } from './invalidBucket.js' import { Queue } from './queue.js' @@ -12,8 +12,10 @@ import type { CreateAutoModerationRuleOptions, CreateChannelInvite, CreateForumPostWithMessage, + CreateGuild, CreateGuildChannel, CreateGuildEmoji, + CreateGuildRole, CreateMessageOptions, CreateScheduledEvent, CreateStageInstance, @@ -30,6 +32,7 @@ import type { DiscordFollowAnnouncementChannel, DiscordFollowedChannel, DiscordGetGatewayBot, + DiscordGuild, DiscordIntegration, DiscordInvite, DiscordInviteMetadata, @@ -38,6 +41,7 @@ import type { DiscordMember, DiscordMemberWithUser, DiscordMessage, + DiscordRole, DiscordScheduledEvent, DiscordStageInstance, DiscordStickerPack, @@ -47,6 +51,7 @@ import type { DiscordWebhook, EditAutoModerationRuleOptions, EditChannelPermissionOverridesOptions, + EditGuildRole, EditMessage, EditScheduledEvent, EditStageInstanceOptions, @@ -64,18 +69,13 @@ import type { ModifyChannel, ModifyGuildChannelPositions, ModifyGuildEmoji, + ModifyRolePositions, ModifyWebhook, SearchMembers, StartThreadWithMessage, StartThreadWithoutMessage, WithReason, - - CreateGuild, - CreateGuildRole, - DiscordGuild, - DiscordRole, - EditGuildRole, - ModifyRolePositions} from '@discordeno/types' +} from '@discordeno/types' import type { InvalidRequestBucket } from './invalidBucket.js' // TODO: make dynamic based on package.json file @@ -536,6 +536,33 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage return false }, + changeToDiscordFormat(obj: any): any { + if (obj === null) return null + + if (typeof obj === 'object') { + if (Array.isArray(obj)) { + return obj.map((item) => rest.changeToDiscordFormat(item)) + } + + const newObj: any = {} + + for (const key of Object.keys(obj)) { + if (key === 'permissions') { + newObj.permissions = '1234567890' + continue + } + + newObj[camelToSnakeCase(key)] = rest.changeToDiscordFormat(obj[key]) + } + + return newObj + } + + if (typeof obj === 'bigint') return obj.toString() + + return obj + }, + createRequest(options) { const headers: Record = { 'user-agent': `DiscordBot (https://github.com/discordeno/discordeno, v${version})`, @@ -561,32 +588,36 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage options.body.reason = undefined } - if (options.body?.file) { - const files = findFiles(options.body.file) - const form = new FormData() + if (options.body) { + const { file } = options.body + if (file) { + const files = findFiles(file) + const form = new FormData() - // WHEN CREATING A STICKER, DISCORD WANTS FORM DATA ONLY - if (options.url?.endsWith('/stickers') && options.method === 'POST') { - form.append('file', files[0].blob, files[0].name) - form.append('name', options.body.name as string) - form.append('description', options.body.description as string) - form.append('tags', options.body.tags as string) - } else { - for (let i = 0; i < files.length; i++) { - form.append(`file${i}`, files[i].blob, files[i].name) + // WHEN CREATING A STICKER, DISCORD WANTS FORM DATA ONLY + if (options.url?.endsWith('/stickers') && options.method === 'POST') { + form.append('file', files[0].blob, files[0].name) + form.append('name', options.body.name as string) + form.append('description', options.body.description as string) + form.append('tags', options.body.tags as string) + } else { + for (let i = 0; i < files.length; i++) { + form.append(`file${i}`, files[i].blob, files[i].name) + } + + if (file) options.body.file = undefined + form.append('payload_json', JSON.stringify(rest.changeToDiscordFormat(options.body))) } - form.append('payload_json', JSON.stringify({ ...options.body, file: undefined })) + options.body.file = form + } else if (options.body && !['GET', 'DELETE'].includes(options.method)) { + headers['Content-Type'] = 'application/json' } - - options.body.file = form - } else if (options.body && !['GET', 'DELETE'].includes(options.method)) { - headers['Content-Type'] = 'application/json' } return { headers, - body: (options.body?.file ?? JSON.stringify(options.body)) as FormData | string, + body: (options.body?.file ?? JSON.stringify(rest.changeToDiscordFormat(options.body))) as FormData | string, method: options.method, } }, @@ -698,11 +729,9 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }, async sendRequest(options) { - console.log('in send request') const url = options.url.startsWith('https://') ? options.url : `${rest.baseUrl}/v${rest.version}${options.url}` const payload = rest.createRequest({ method: options.method, url: options.url, body: options.body, ...options.options }) - console.log(`sending request to ${url}`, 'with payload:', { ...payload, headers: { ...payload.headers, authorization: 'Bot tokenhere' } }) logger.debug(`sending request to ${url}`, 'with payload:', { ...payload, headers: { ...payload.headers, authorization: 'Bot tokenhere' } }) const response = await fetch(url, payload) logger.debug(`request fetched from ${url} with status ${response.status} & ${response.statusText}`) @@ -736,6 +765,8 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage return await options.retryRequest?.(options) } + + return options.reject(await response.json()) } // Discord sometimes sends no response @@ -1407,7 +1438,6 @@ export function createRestManager(options: CreateRestManagerOptions): RestManage }, async createGuild(options) { - console.log('in create guild') return await rest.post(rest.routes.guilds.all(), options) }, @@ -2170,6 +2200,8 @@ export interface RestManager { } /** Check the rate limits for a url or a bucket. */ checkRateLimits: (url: string) => number | false + /** Reshapes and modifies the obj as needed to make it ready for discords api. */ + changeToDiscordFormat: (obj: any) => any /** Creates the request body and headers that are necessary to send a request. Will handle different types of methods and everything necessary for discord. */ createRequest: (options: CreateRequestBodyOptions) => RequestBody /** This will create a infinite loop running in 1 seconds using tail recursion to keep rate limits clean. When a rate limit resets, this will remove it so the queue can proceed. */ diff --git a/packages/rest/tests/e2e/message.spec.ts b/packages/rest/tests/e2e/message.spec.ts index 9e91e0ae8..a284eff11 100644 --- a/packages/rest/tests/e2e/message.spec.ts +++ b/packages/rest/tests/e2e/message.spec.ts @@ -1,6 +1,19 @@ import { expect } from 'chai' import { describe, it } from 'mocha' -import { rest } from './utils.js' +import { e2ecache, rest } from './utils.js' + +before(async () => { + if (!e2ecache.guild) { + e2ecache.guild = await rest.createGuild({ + name: 'Discordeno-test', + }) + } +}) + +after(async () => { + if (rest.invalidBucket.timeoutId) clearTimeout(rest.invalidBucket.timeoutId) + if (e2ecache.guild.id) await rest.deleteGuild(e2ecache.guild.id) +}) describe('[rest] Message related tests', () => { describe('Send a message', () => { diff --git a/packages/rest/tests/e2e/role.spec.ts b/packages/rest/tests/e2e/role.spec.ts index 07cf216c5..1e6e3d3e9 100644 --- a/packages/rest/tests/e2e/role.spec.ts +++ b/packages/rest/tests/e2e/role.spec.ts @@ -4,13 +4,18 @@ import { expect } from 'chai' import { afterEach, before, beforeEach, describe, it } from 'mocha' import { e2ecache, rest } from './utils.js' -// before(async () => { -// if (!e2ecache.guild) { -// e2ecache.guild = await rest.createGuild({ -// name: 'Discordeno-test' -// }) -// } -// }) +before(async () => { + if (!e2ecache.guild) { + e2ecache.guild = await rest.createGuild({ + name: 'Discordeno-test', + }) + } +}) + +after(async () => { + if (rest.invalidBucket.timeoutId) clearTimeout(rest.invalidBucket.timeoutId) + if (e2ecache.guild.id) await rest.deleteGuild(e2ecache.guild.id) +}) describe('[role] Role tests', async () => { // Create a role with a reason @@ -18,9 +23,9 @@ describe('[role] Role tests', async () => { const role = await rest.createRole( e2ecache.guild?.id, { - name: `test role ${Date.now()}` + name: `test role ${Date.now()}`, }, - 'test reason' + 'test reason', ) expect(role.id).to.exist @@ -31,7 +36,7 @@ describe('[role] Role tests', async () => { // Create a role without a reason it('Create a role without a reason', async () => { const role = await rest.createRole(e2ecache.guild.id, { - name: `test role ${Date.now()}` + name: `test role ${Date.now()}`, }) expect(role.id).to.exist @@ -42,11 +47,11 @@ describe('[role] Role tests', async () => { // Delete a role it('Delete a role', async () => { const role = await rest.createRole(e2ecache.guild.id, { - name: `test role ${Date.now()}` + name: `test role ${Date.now()}`, }) await rest.deleteRole(e2ecache.guild.id, role.id) const deletedRoles = await rest.getRoles(e2ecache.guild.id) - expect(deletedRoles.some(r => r.id === role.id)).to.equal(false) + expect(deletedRoles.some((r) => r.id === role.id)).to.equal(false) }) // Edit a role @@ -55,7 +60,7 @@ describe('[role] Role tests', async () => { beforeEach(async () => { role = await rest.createRole(e2ecache.guild.id, { - name: `test role ${Date.now()}` + name: `test role ${Date.now()}`, }) }) @@ -66,7 +71,7 @@ describe('[role] Role tests', async () => { // Edit the roles name it('Edit the roles name', async () => { const edited = await rest.editRole(e2ecache.guild.id, role.id, { - name: 'test role 4' + name: 'test role 4', }) expect(edited.name).to.equal('test role 4') }) @@ -74,7 +79,7 @@ describe('[role] Role tests', async () => { // Edit the roles color it('Edit the roles color', async () => { const edited = await rest.editRole(e2ecache.guild.id, role.id, { - color: 0x0000ff + color: 0x0000ff, }) expect(edited.color).to.equal(0x0000ff) }) @@ -121,11 +126,9 @@ describe('[role] Role tests', async () => { // Edit the roles permissions it('Edit the roles permissions', async () => { const edited = await rest.editRole(e2ecache.guild.id, role.id, { - permissions: ['SEND_MESSAGES', 'VIEW_CHANNEL'] + permissions: ['SEND_MESSAGES', 'VIEW_CHANNEL'], }) - expect(edited.permissions.toString()).to.equal( - calculateBits(['SEND_MESSAGES', 'VIEW_CHANNEL']) - ) + expect(edited.permissions.toString()).to.equal(calculateBits(['SEND_MESSAGES', 'VIEW_CHANNEL'])) }) }) @@ -134,13 +137,9 @@ describe('[role] Role tests', async () => { beforeEach(async () => { role = await rest.createRole(e2ecache.guild.id, { - name: `test role ${Date.now()}` + name: `test role ${Date.now()}`, }) - await rest.addRole( - e2ecache.guild.id, - 130136895395987456n, - role.id - ) + await rest.addRole(e2ecache.guild.id, rest.applicationId, role.id) }) afterEach(async () => { @@ -148,29 +147,15 @@ describe('[role] Role tests', async () => { }) it('without a reason', async () => { - await rest.removeRole( - e2ecache.guild.id, - 130136895395987456n, - role.id - ) - const member = await rest.getMember( - e2ecache.guild.id, - 130136895395987456n - ) + await rest.removeRole(e2ecache.guild.id, rest.applicationId, role.id) + const member = await rest.getMember(e2ecache.guild.id, rest.applicationId) + // console.log('member', member.errors.userId.Errors) expect(member?.roles.includes(role.id)).to.equal(false) }) it('with a reason', async () => { - await rest.removeRole( - e2ecache.guild.id, - 130136895395987456n, - role.id, - 'test reason' - ) - const member = await rest.getMember( - e2ecache.guild.id, - 130136895395987456n - ) + await rest.removeRole(e2ecache.guild.id, rest.applicationId, role.id, 'test reason') + const member = await rest.getMember(e2ecache.guild.id, rest.applicationId) expect(member?.roles.includes(role.id)).to.equal(false) }) }) @@ -180,45 +165,26 @@ describe('[role] Role tests', async () => { beforeEach(async () => { role = await rest.createRole(e2ecache.guild.id, { - name: `test role ${Date.now()}` + name: `test role ${Date.now()}`, }) }) afterEach(async () => { - await rest.removeRole( - e2ecache.guild.id, - 130136895395987456n, - role.id - ) + await rest.removeRole(e2ecache.guild.id, rest.applicationId, role.id) await rest.deleteRole(e2ecache.guild.id, role.id) }) it('Without a reason.', async () => { // Assign the role to the user - await rest.addRole( - e2ecache.guild.id, - 130136895395987456n, - role.id - ) - const member = await rest.getMember( - e2ecache.guild.id, - 130136895395987456n - ) + await rest.addRole(e2ecache.guild.id, rest.applicationId, role.id) + const member = await rest.getMember(e2ecache.guild.id, rest.applicationId) expect(member?.roles.includes(role.id)).to.equal(true) }) // Add the role to the user with a reason it('With a reason', async () => { - await rest.addRole( - e2ecache.guild.id, - 130136895395987456n, - role.id, - 'test reason' - ) - const member = await rest.getMember( - e2ecache.guild.id, - 130136895395987456n - ) + await rest.addRole(e2ecache.guild.id, rest.applicationId, role.id, 'test reason') + const member = await rest.getMember(e2ecache.guild.id, rest.applicationId) expect(member?.roles.includes(role.id)).to.equal(true) }) }) diff --git a/packages/rest/tests/e2e/user.spec.ts b/packages/rest/tests/e2e/user.spec.ts index 3e74f6406..0adf18c90 100644 --- a/packages/rest/tests/e2e/user.spec.ts +++ b/packages/rest/tests/e2e/user.spec.ts @@ -1,9 +1,22 @@ import chai, { expect } from 'chai' import chaiAsPromised from 'chai-as-promised' import { describe, it } from 'mocha' -import { rest } from './utils.js' +import { e2ecache, rest } from './utils.js' chai.use(chaiAsPromised) +before(async () => { + if (!e2ecache.guild) { + e2ecache.guild = await rest.createGuild({ + name: 'Discordeno-test', + }) + } +}) + +after(async () => { + if (rest.invalidBucket.timeoutId) clearTimeout(rest.invalidBucket.timeoutId) + if (e2ecache.guild.id) await rest.deleteGuild(e2ecache.guild.id) +}) + describe('[rest] User related tests', () => { describe('Get a user from the api', () => { it('With a valid user id', async () => { diff --git a/packages/rest/tests/e2e/utils.ts b/packages/rest/tests/e2e/utils.ts index 4cc3e92a3..19079cecc 100644 --- a/packages/rest/tests/e2e/utils.ts +++ b/packages/rest/tests/e2e/utils.ts @@ -1,18 +1,16 @@ -import { logger, LogLevels } from '@discordeno/utils' +import { LogDepth, logger, LogLevels } from '@discordeno/utils' import { createRestManager } from '../../src/manager.js' import { token } from './constants.js' // For debugging purposes -logger.setLevel(LogLevels.Debug) +// logger.setLevel(LogLevels.Debug) +// logger.setDepth(LogDepth.Full) export const rest = createRestManager({ token, }) rest.deleteQueueDelay = 10000 -console.log('CREATING GUILD') export const e2ecache = { guild: await rest.createGuild({ name: 'ddenotester' }), } - -console.log('CACHED check', e2ecache.guild) diff --git a/packages/rest/tests/e2e/xyz.spec.ts b/packages/rest/tests/e2e/xyz.spec.ts deleted file mode 100644 index 8b38930cf..000000000 --- a/packages/rest/tests/e2e/xyz.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import chai from 'chai' -import chaiAsPromised from 'chai-as-promised' -import { describe, it } from 'mocha' -import { e2ecache, rest } from './utils.js' -chai.use(chaiAsPromised) - -// The xyz.spec.ts file name will make this test run last as tests are ran in alphabetical file name order. - -describe('[rest] Cleanup tests', () => { - describe('Remove the timers', () => { - it('In the invalid bucket', async () => { - if (rest.invalidBucket.timeoutId) clearTimeout(rest.invalidBucket.timeoutId) - }) - }) - - it('Delete the created guild', async () => { - if (e2ecache.guild.id) await rest.deleteGuild(e2ecache.guild.id); - }) -}) diff --git a/packages/utils/src/casing.ts b/packages/utils/src/casing.ts index a9d3bb0ea..a38383dbf 100644 --- a/packages/utils/src/casing.ts +++ b/packages/utils/src/casing.ts @@ -32,3 +32,18 @@ export function snakeToCamelCase(str: string): string { return result } + +export function camelToSnakeCase(str: string): string { + let result = ""; + for (let i = 0, len = str.length; i < len; ++i) { + if (str[i] >= "A" && str[i] <= "Z") { + result += `_${str[i].toLowerCase()}`; + + continue; + } + + result += str[i]; + } + + return result; +}