From 557a5838264931c8962c56acc488e5b03c0475d8 Mon Sep 17 00:00:00 2001 From: Andrey Grunev Date: Fri, 15 May 2020 12:42:36 +0300 Subject: [PATCH 01/37] Fixes error TS2783: 'type' is specified more than once --- structures/guild.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structures/guild.ts b/structures/guild.ts index a9a2d000d..537fc4886 100644 --- a/structures/guild.ts +++ b/structures/guild.ts @@ -82,7 +82,6 @@ export const createGuild = (data: CreateGuildPayload) => { const result = (await RequestManager.post(endpoints.GUILD_CHANNELS(data.id), { name, - type: options.type ? ChannelTypes[options.type] : undefined, permission_overwrites: options?.permission_overwrites ? options.permission_overwrites.map((perm) => ({ ...perm, @@ -91,6 +90,7 @@ export const createGuild = (data: CreateGuildPayload) => { })) : undefined, ...options, + type: options.type ? ChannelTypes[options.type] : undefined, })) as ChannelCreatePayload; const channel = createChannel(result); From 8119ac2dccaee95505699879249be043e0671183 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Fri, 15 May 2020 08:32:37 -0400 Subject: [PATCH 02/37] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ff6940db3..044a7a2eb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,7 +3,7 @@ name: Bug report about: Create a report to help us improve title: '' labels: bug -assignees: Skillz4Killz, resynth1943 +assignees: Skillz4Killz --- From fc1959d06d970dfcf3654c0def38f679bebe14fd Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Fri, 15 May 2020 09:24:30 -0400 Subject: [PATCH 03/37] Update greetings.yml --- .github/workflows/greetings.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index fc1b76399..09b29e9c6 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -9,5 +9,5 @@ jobs: - uses: actions/first-interaction@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: 'Thank you for helping contribute to Discordeno. I really do appreciate any and all contributions! Hopefully, together we will be able to make the very best gaming bot in the world. Since, this is your very first issue, feel free to look around the repository and then hop on into the Discord server, where you can chat with me directly. https://discord.gg/J4NqJ72' - pr-message: 'Thank you for helping contribute to Discordeno. I really do appreciate any and all contributions! Hopefully, together we will be able to make the very best gaming bot in the world. Since, this is your very first pull request, feel free to look around the repository and then hop on into the Discord server, where you can chat with me directly. https://discord.gg/J4NqJ72' + issue-message: 'Thank you for helping contribute to Discordeno. I really do appreciate any and all contributions! Hopefully, together we will be able to make the very best bot Discord API module in the world. Since, this is your very first issue, feel free to look around the repository and then hop on into the Discord server, where you can chat with me directly. https://discord.gg/J4NqJ72' + pr-message: 'Thank you for helping contribute to Discordeno. I really do appreciate any and all contributions! Hopefully, together we will be able to make the very best Discord API module in the world. Since, this is your very first pull request, feel free to look around the repository and then hop on into the Discord server, where you can chat with me directly. https://discord.gg/J4NqJ72' From be3730c9be24d039a4bd6d8368b3e1fcb82eacda Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Fri, 15 May 2020 13:28:19 -0400 Subject: [PATCH 04/37] Update cdn.ts --- utils/cdn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/cdn.ts b/utils/cdn.ts index 442979f34..927093f6a 100644 --- a/utils/cdn.ts +++ b/utils/cdn.ts @@ -7,5 +7,5 @@ export const formatImageURL = ( ) => { return `${url}.${ format || url.includes("/a_") ? "gif" : "jpg" - }/?size=${size}`; + }?size=${size}`; }; From 32681807f7de0ac6ea3b7fb3e1fe4d7d06591afd Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Fri, 15 May 2020 13:28:40 -0400 Subject: [PATCH 05/37] Update cdn.ts --- utils/cdn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/cdn.ts b/utils/cdn.ts index 442979f34..927093f6a 100644 --- a/utils/cdn.ts +++ b/utils/cdn.ts @@ -7,5 +7,5 @@ export const formatImageURL = ( ) => { return `${url}.${ format || url.includes("/a_") ? "gif" : "jpg" - }/?size=${size}`; + }?size=${size}`; }; From 44a1c428f2db623022c4e5a66881b621765c11af Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Fri, 15 May 2020 22:29:29 -0400 Subject: [PATCH 06/37] Update message.ts --- structures/message.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/structures/message.ts b/structures/message.ts index dc905e8a9..e19518a1b 100644 --- a/structures/message.ts +++ b/structures/message.ts @@ -14,6 +14,7 @@ export function createMessage(data: MessageCreateOptions) { return { ...data, raw: data, + mentions: data.mentions.map(user => createUser(user)), author: createUser({ ...data.author, avatar: data.author.avatar || "" }), timestamp: Date.parse(data.timestamp), editedTimestamp: data.edited_timestamp From b7622f708500a21aa371919f2e4e216608ca6d51 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Fri, 15 May 2020 22:30:04 -0400 Subject: [PATCH 07/37] Update message.ts --- structures/message.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/structures/message.ts b/structures/message.ts index dc905e8a9..e19518a1b 100644 --- a/structures/message.ts +++ b/structures/message.ts @@ -14,6 +14,7 @@ export function createMessage(data: MessageCreateOptions) { return { ...data, raw: data, + mentions: data.mentions.map(user => createUser(user)), author: createUser({ ...data.author, avatar: data.author.avatar || "" }), timestamp: Date.parse(data.timestamp), editedTimestamp: data.edited_timestamp From e511c91014a674f1c44687a7642727ed36a51622 Mon Sep 17 00:00:00 2001 From: BinotaLIU Date: Sat, 16 May 2020 20:29:03 +0800 Subject: [PATCH 08/37] add deno fmt --check to github action --- .github/workflows/deno.yml | 29 ++++++++++++++++------------- .github/workflows/greetings.yml | 10 +++++----- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml index aeae72c3c..31a01f89b 100644 --- a/.github/workflows/deno.yml +++ b/.github/workflows/deno.yml @@ -2,13 +2,13 @@ name: Testing/Linting -# Controls when the action will run. Triggers the workflow on push or pull request +# Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -19,14 +19,17 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - - - name: Setup project - run: cp configs.ts.example configs.ts - - - name: Setup Deno environment - uses: denolib/setup-deno@master + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 - - name: Deno Fetch - run: deno cache mod.ts + - name: Setup project + run: cp configs.ts.example configs.ts + + - name: Setup Deno environment + uses: denolib/setup-deno@master + + - name: Deno Fetch + run: deno cache mod.ts + + - name: Deno Format Check + run: deno fmt --check diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 09b29e9c6..d65aeb3b4 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -6,8 +6,8 @@ jobs: greeting: runs-on: ubuntu-latest steps: - - uses: actions/first-interaction@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: 'Thank you for helping contribute to Discordeno. I really do appreciate any and all contributions! Hopefully, together we will be able to make the very best bot Discord API module in the world. Since, this is your very first issue, feel free to look around the repository and then hop on into the Discord server, where you can chat with me directly. https://discord.gg/J4NqJ72' - pr-message: 'Thank you for helping contribute to Discordeno. I really do appreciate any and all contributions! Hopefully, together we will be able to make the very best Discord API module in the world. Since, this is your very first pull request, feel free to look around the repository and then hop on into the Discord server, where you can chat with me directly. https://discord.gg/J4NqJ72' + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: "Thank you for helping contribute to Discordeno. I really do appreciate any and all contributions! Hopefully, together we will be able to make the very best bot Discord API module in the world. Since, this is your very first issue, feel free to look around the repository and then hop on into the Discord server, where you can chat with me directly. https://discord.gg/J4NqJ72" + pr-message: "Thank you for helping contribute to Discordeno. I really do appreciate any and all contributions! Hopefully, together we will be able to make the very best Discord API module in the world. Since, this is your very first pull request, feel free to look around the repository and then hop on into the Discord server, where you can chat with me directly. https://discord.gg/J4NqJ72" From ded7c3c395fb744226c5d0118d6580b5e040d4d7 Mon Sep 17 00:00:00 2001 From: BinotaLIU Date: Sat, 16 May 2020 20:29:48 +0800 Subject: [PATCH 09/37] add .editorconfig --- .editorconfig | 9 +++++++++ .prettierrc | 4 ---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .editorconfig delete mode 100644 .prettierrc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..a05749c25 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index afa7d068e..000000000 --- a/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "semi": false, - "printWidth": 120 -} From 8a8ba17889f2e7e2d5b50731bf60c259a21162ab Mon Sep 17 00:00:00 2001 From: BinotaLIU Date: Sat, 16 May 2020 20:35:25 +0800 Subject: [PATCH 10/37] formatting --- mod.ts | 1 - module/requestManager.ts | 10 +++++----- structures/guild.ts | 8 +++++--- structures/message.ts | 2 +- types/errors.ts | 2 +- utils/cdn.ts | 4 +--- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/mod.ts b/mod.ts index 6e28e9fd9..be579876f 100644 --- a/mod.ts +++ b/mod.ts @@ -17,7 +17,6 @@ Client({ if (!message.guild_id) return; const guild = cache.guilds.get(message.guild_id); if (!guild) return logYellow("no guild"); - } } }, diff --git a/module/requestManager.ts b/module/requestManager.ts index e4b46d91f..48ac5026d 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -60,7 +60,7 @@ export const RequestManager = { }, }; -function createRequestBody (body: any, method?: RequestMethod) { +function createRequestBody(body: any, method?: RequestMethod) { return { headers: { Authorization: authorization, @@ -72,7 +72,7 @@ function createRequestBody (body: any, method?: RequestMethod) { body: JSON.stringify(body), method: method?.toUpperCase(), }; -}; +} async function checkRatelimits(url: string) { const ratelimited = ratelimitedPaths.get(url); @@ -109,7 +109,7 @@ async function runMethod( ) { if (retryCount > 10) throw new Error(Errors.RATE_LIMIT_RETRY_MAXED); await delay(json.retry_after); - return runMethod(method, url, body, retryCount++) + return runMethod(method, url, body, retryCount++); } return resolve(json); @@ -124,7 +124,7 @@ async function runMethod( processQueue(); } }); -}; +} function processHeaders(url: string, headers: Headers) { // If a rate limit response is encountered this will become true and returned @@ -160,4 +160,4 @@ function processHeaders(url: string, headers: Headers) { // Returns a boolean to check if we need to request again once the rate limit resets return ratelimited; -}; +} diff --git a/structures/guild.ts b/structures/guild.ts index e0da2444f..f45e67218 100644 --- a/structures/guild.ts +++ b/structures/guild.ts @@ -252,11 +252,13 @@ export const createGuild = (data: CreateGuildPayload) => { return RequestManager.post(endpoints.GUILD_PRUNE(data.id), { days }); }, fetchMembers: (options?: FetchMembersOptions) => { - if (!(identifyPayload.intents & Intents.GUILD_MEMBERS)) throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS) + if (!(identifyPayload.intents & Intents.GUILD_MEMBERS)) { + throw new Error(Errors.MISSING_INTENT_GUILD_MEMBERS); + } return new Promise((resolve) => { - requestAllMembers(data.id, resolve, guild.memberCount, options) - }) + requestAllMembers(data.id, resolve, guild.memberCount, options); + }); }, /** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */ getAuditLogs: (options: GetAuditLogsOptions) => { diff --git a/structures/message.ts b/structures/message.ts index e19518a1b..1198ed0c0 100644 --- a/structures/message.ts +++ b/structures/message.ts @@ -14,7 +14,7 @@ export function createMessage(data: MessageCreateOptions) { return { ...data, raw: data, - mentions: data.mentions.map(user => createUser(user)), + mentions: data.mentions.map((user) => createUser(user)), author: createUser({ ...data.author, avatar: data.author.avatar || "" }), timestamp: Date.parse(data.timestamp), editedTimestamp: data.edited_timestamp diff --git a/types/errors.ts b/types/errors.ts index 2aec5a375..384393480 100644 --- a/types/errors.ts +++ b/types/errors.ts @@ -22,5 +22,5 @@ export enum Errors { NICKNAMES_MAX_LENGTH = "NICKNAMES_MAX_LENGTH", PRUNE_MIN_DAYS = "PRUNE_MIN_DAYS", RATE_LIMIT_RETRY_MAXED = "RATE_LIMIT_RETRY_MAXED", - MISSING_INTENT_GUILD_MEMBERS = "MISSING_INTENT_GUILD_MEMBERS" + MISSING_INTENT_GUILD_MEMBERS = "MISSING_INTENT_GUILD_MEMBERS", } diff --git a/utils/cdn.ts b/utils/cdn.ts index 927093f6a..de1eb6565 100644 --- a/utils/cdn.ts +++ b/utils/cdn.ts @@ -5,7 +5,5 @@ export const formatImageURL = ( size: ImageSize = 128, format?: ImageFormats, ) => { - return `${url}.${ - format || url.includes("/a_") ? "gif" : "jpg" - }?size=${size}`; + return `${url}.${format || url.includes("/a_") ? "gif" : "jpg"}?size=${size}`; }; From a5a6dfde6c0b9f7dfd861d7735b4069258776d76 Mon Sep 17 00:00:00 2001 From: NTM Nathan <34463340+NTMNathan@users.noreply.github.com> Date: Sat, 16 May 2020 22:47:37 +1000 Subject: [PATCH 11/37] Open Sourced Bots Table Added a table with the Known Open Sourced Bots created with Discordeno. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7012331f2..39c6222c0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,11 @@ Discord API library wrapper in Deno If you are just starting out, you can use the Discordeno Template repo to get the base of your bot pre-built for you. As other developers create other command frameworks for this library, those frameworks will be listed here: -**[Official Boilerplate](https://github.com/Skillz4Killz/Discordeno-bot-template):** This is a very minimalistic design for a boilerplate for your bot to get you started. +| Bot Name | Developer | Repository | Support Server | Invite URL | +|--------------------|--------------------|---------------------------------------------------------|------------------------------------|------------| +| Discordeno Example | Skillz4Killz #4500 | https://github.com/Skillz4Killz/Discordeno-bot-template | https://discord.gg/J4NqJ72 | N/A | +| DenoBot | NTM Nathan#0001 | https://github.com/ntm-development/DenoBot | https://discord.com/invite/G2rb53z | N/A | +| discordeno-mattis | Mattis6666 | https://github.com/Mattis6666/discordeno-mattis/ | N/A | N/A | ## Motivations/Features From a6ad93a1d6a11c53caa173ca89f3db8934bc346c Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Sat, 16 May 2020 08:56:33 -0400 Subject: [PATCH 12/37] Update README.md --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 39c6222c0..425289c65 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,14 @@ Discord API library wrapper in Deno If you are just starting out, you can use the Discordeno Template repo to get the base of your bot pre-built for you. As other developers create other command frameworks for this library, those frameworks will be listed here: -| Bot Name | Developer | Repository | Support Server | Invite URL | -|--------------------|--------------------|---------------------------------------------------------|------------------------------------|------------| -| Discordeno Example | Skillz4Killz #4500 | https://github.com/Skillz4Killz/Discordeno-bot-template | https://discord.gg/J4NqJ72 | N/A | -| DenoBot | NTM Nathan#0001 | https://github.com/ntm-development/DenoBot | https://discord.com/invite/G2rb53z | N/A | -| discordeno-mattis | Mattis6666 | https://github.com/Mattis6666/discordeno-mattis/ | N/A | N/A | +**[Official Boilerplate](https://github.com/Skillz4Killz/Discordeno-bot-template):** This is a very minimalistic design for a boilerplate for your bot to get you started. + +## Open Source Bots Using Discordeno + +| Bot Name | Developer | Links | +|--------------------|--------------------|---------------------------------------------------------| +| DenoBot | NTM Nathan#0001 | [Github](https://github.com/ntm-development/DenoBot) [Support Server](https://discord.com/invite/G2rb53z) | +| discordeno-mattis | Mattis6666 | [Github](https://github.com/Mattis6666/discordeno-mattis/) | ## Motivations/Features From 7b9f2d3ec267bd537c1ec87d01e8552d563ea1bd Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Sat, 16 May 2020 08:56:53 -0400 Subject: [PATCH 13/37] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 425289c65..7b50c0324 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ If you are just starting out, you can use the Discordeno Template repo to get th | Bot Name | Developer | Links | |--------------------|--------------------|---------------------------------------------------------| -| DenoBot | NTM Nathan#0001 | [Github](https://github.com/ntm-development/DenoBot) [Support Server](https://discord.com/invite/G2rb53z) | +| DenoBot | NTM Nathan#0001 | [Github](https://github.com/ntm-development/DenoBot), [Support Server](https://discord.com/invite/G2rb53z) | | discordeno-mattis | Mattis6666 | [Github](https://github.com/Mattis6666/discordeno-mattis/) | ## Motivations/Features From 8a298d0b6d1c939996445a9d6dbd1f8d7c282b85 Mon Sep 17 00:00:00 2001 From: BinotaLIU Date: Sat, 16 May 2020 21:08:25 +0800 Subject: [PATCH 14/37] add handleStatusCode --- module/requestManager.ts | 27 ++++++++++++++++++++++++++- types/discord.ts | 6 +++--- types/errors.ts | 3 +++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/module/requestManager.ts b/module/requestManager.ts index 48ac5026d..12e7f1f51 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -2,6 +2,7 @@ import { RequestMethod } from "../types/fetch.ts"; import { authorization } from "./client.ts"; import { delay } from "https://deno.land/std@0.50.0/async/delay.ts"; import { Errors } from "../types/errors.ts"; +import { HttpResponseCode } from "../types/discord.ts"; const queue: Array<() => Promise> = []; const ratelimitedPaths = new Map(); @@ -42,6 +43,7 @@ export const RequestManager = { get: async (url: string, body?: unknown) => { await checkRatelimits(url); const result = await fetch(url, createRequestBody(body)); + handleStatusCode(result.status); processHeaders(url, result.headers); return result.json(); @@ -99,13 +101,15 @@ async function runMethod( await checkRatelimits(url); const response = await fetch(url, createRequestBody(body, method)); processHeaders(url, response.headers); + handleStatusCode(response.status); // Sometimes Discord returns an empty 204 response that can't be made to JSON. if (response.status === 204) resolve(); const json = await response.json(); if ( - json.retry_after || json.message === "You are being rate limited." + json.retry_after || + json.message === "You are being rate limited." ) { if (retryCount > 10) throw new Error(Errors.RATE_LIMIT_RETRY_MAXED); await delay(json.retry_after); @@ -126,6 +130,27 @@ async function runMethod( }); } +function handleStatusCode(status: number): boolean { + if (status >= 200 && status < 400) { + return true; + } + + switch (status) { + case HttpResponseCode.BadRequest: + case HttpResponseCode.Unauthorized: + case HttpResponseCode.Forbidden: + case HttpResponseCode.NotFound: + case HttpResponseCode.MethodNotAllowed: + case HttpResponseCode.TooManyRequests: + throw new Error(Errors.REQUEST_CLIENT_ERROR); + case HttpResponseCode.GatewayUnavailable: + throw new Error(Errors.REQUEST_SERVER_ERROR); + } + + // left are all unknown + throw new Error(Errors.REQUEST_UNKNOWN_ERROR); +} + function processHeaders(url: string, headers: Headers) { // If a rate limit response is encountered this will become true and returned let ratelimited = false; diff --git a/types/discord.ts b/types/discord.ts index 5777ee238..f927877ea 100644 --- a/types/discord.ts +++ b/types/discord.ts @@ -92,14 +92,14 @@ export enum VoiceCloseEventCode { export enum HttpResponseCode { Ok = 200, - Created, + Created = 201, NoContent = 204, NotModified = 304, BadRequest = 400, Unauthorized = 401, Forbidden = 403, - NotFound, - MethodNotAllowed, + NotFound = 404, + MethodNotAllowed = 405, TooManyRequests = 429, GatewayUnavailable = 502, // ServerError left untyped because it's 5xx. diff --git a/types/errors.ts b/types/errors.ts index 384393480..c91cc1fad 100644 --- a/types/errors.ts +++ b/types/errors.ts @@ -23,4 +23,7 @@ export enum Errors { PRUNE_MIN_DAYS = "PRUNE_MIN_DAYS", RATE_LIMIT_RETRY_MAXED = "RATE_LIMIT_RETRY_MAXED", MISSING_INTENT_GUILD_MEMBERS = "MISSING_INTENT_GUILD_MEMBERS", + REQUEST_CLIENT_ERROR = "REQUEST_CLIENT_ERROR", + REQUEST_SERVER_ERROR = "REQUEST_SERVER_ERROR", + REQUEST_UNKNOWN_ERROR = "REQUEST_UNKNOWN_ERROR", } From c9b901cdc0a7eb0a682d88d21a3aa42477d94b24 Mon Sep 17 00:00:00 2001 From: Skillz Date: Sat, 16 May 2020 09:55:25 -0400 Subject: [PATCH 15/37] get properly handled with request manager --- .vscode/settings.json | 1 + module/client.ts | 2 +- module/requestManager.ts | 11 +++-------- structures/channel.ts | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fe8877c72..ca292ec77 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "deno.enable": true, "editor.formatOnSave": true, + "deno.autoFmtOnSave": true, "deno.unstable": true } diff --git a/module/client.ts b/module/client.ts index 6009da90f..be1ba74ef 100644 --- a/module/client.ts +++ b/module/client.ts @@ -33,7 +33,7 @@ export const createClient = async (data: ClientOptions) => { authorization = `Bot ${data.token}`; // Initial API connection to get info about bots connection - botGatewayData = await RequestManager.get(endpoints.GATEWAY_BOT); + botGatewayData = await RequestManager.get(endpoints.GATEWAY_BOT) as DiscordBotGatewayData; identifyPayload.token = data.token; identifyPayload.intents = data.intents.reduce( diff --git a/module/requestManager.ts b/module/requestManager.ts index 12e7f1f51..bb7391d2b 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -41,12 +41,7 @@ processRateLimitedPaths(); export const RequestManager = { // Something off about using runMethod with get breaks when using fetch get: async (url: string, body?: unknown) => { - await checkRatelimits(url); - const result = await fetch(url, createRequestBody(body)); - handleStatusCode(result.status); - processHeaders(url, result.headers); - - return result.json(); + return runMethod(RequestMethod.Get, url, body) }, post: (url: string, body?: unknown) => { return runMethod(RequestMethod.Post, url, body); @@ -62,7 +57,7 @@ export const RequestManager = { }, }; -function createRequestBody(body: any, method?: RequestMethod) { +function createRequestBody(body: any, method: RequestMethod) { return { headers: { Authorization: authorization, @@ -72,7 +67,7 @@ function createRequestBody(body: any, method?: RequestMethod) { "X-Audit-Log-Reason": body ? encodeURIComponent(body.reason) : "", }, body: JSON.stringify(body), - method: method?.toUpperCase(), + method: method.toUpperCase(), }; } diff --git a/structures/channel.ts b/structures/channel.ts index 4db14c4ff..f4da7c327 100644 --- a/structures/channel.ts +++ b/structures/channel.ts @@ -59,7 +59,7 @@ export function createChannel(data: ChannelCreatePayload) { } const result = await RequestManager.get( endpoints.CHANNEL_MESSAGE(data.id, id), - ); + ) as MessageCreateOptions; return createMessage(result); }, /** Fetches between 2-100 messages. Requires VIEW_CHANNEL and READ_MESSAGE_HISTORY */ From 599429988c6cc85ca3d6ff95dcab33bde8657e19 Mon Sep 17 00:00:00 2001 From: Skillz Date: Sat, 16 May 2020 10:32:30 -0400 Subject: [PATCH 16/37] add bot status and fix github action --- mod.ts | 12 +- module/client.ts | 5 +- module/requestManager.ts | 2 +- module/shard.ts | 17 ++ module/shardingManager.ts | 9 + types/activity.ts | 4 + types/guild.ts | 471 +++++++++++++++++++------------------- types/permission.ts | 62 ++--- utils/utils.ts | 12 + 9 files changed, 326 insertions(+), 268 deletions(-) diff --git a/mod.ts b/mod.ts index be579876f..a15587f23 100644 --- a/mod.ts +++ b/mod.ts @@ -3,13 +3,23 @@ import { configs } from "./configs.ts"; import { Intents } from "./types/options.ts"; import { logYellow } from "./utils/logger.ts"; import { cache } from "./utils/cache.ts"; +import { editBotsStatus } from "./utils/utils.ts"; +import { StatusType } from "./types/discord.ts"; +import { ActivityType } from "./types/activity.ts"; Client({ token: configs.token, botID: "675412054529540107", intents: [Intents.GUILDS, Intents.GUILD_MESSAGES, Intents.GUILD_MEMBERS], eventHandlers: { - ready: () => logYellow("Bot ready emitted"), + ready: () => { + logYellow("Bot ready emitted"); + editBotsStatus( + StatusType.DoNotDisturb, + "Testing Name DND", + ActivityType.Listening, + ); + }, // raw: (data) => logGreen("[RAW] => " + JSON.stringify(data)), messageCreate: async (message) => { if (message.author.id === "130136895395987456") { diff --git a/module/client.ts b/module/client.ts index be1ba74ef..88e2cc0c5 100644 --- a/module/client.ts +++ b/module/client.ts @@ -5,7 +5,6 @@ import { RequestManager } from "./requestManager.ts"; import { Channel } from "../structures/channel.ts"; import { spawnShards } from "./shardingManager.ts"; import { cache } from "../utils/cache.ts"; -// // USELESS_ARG_TO_MAKE_DENO_CACHE_WORK // import "./shard.ts"; export let authorization = ""; @@ -33,7 +32,9 @@ export const createClient = async (data: ClientOptions) => { authorization = `Bot ${data.token}`; // Initial API connection to get info about bots connection - botGatewayData = await RequestManager.get(endpoints.GATEWAY_BOT) as DiscordBotGatewayData; + botGatewayData = await RequestManager.get( + endpoints.GATEWAY_BOT, + ) as DiscordBotGatewayData; identifyPayload.token = data.token; identifyPayload.intents = data.intents.reduce( diff --git a/module/requestManager.ts b/module/requestManager.ts index bb7391d2b..edbb4fece 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -41,7 +41,7 @@ processRateLimitedPaths(); export const RequestManager = { // Something off about using runMethod with get breaks when using fetch get: async (url: string, body?: unknown) => { - return runMethod(RequestMethod.Get, url, body) + return runMethod(RequestMethod.Get, url, body); }, post: (url: string, body?: unknown) => { return runMethod(RequestMethod.Post, url, body); diff --git a/module/shard.ts b/module/shard.ts index 6793443f7..376e7fe2e 100644 --- a/module/shard.ts +++ b/module/shard.ts @@ -145,5 +145,22 @@ if (typeof self.onmessage === "function") { message.data.options, ); } + + if (message.data.type === "EDIT_BOTS_STATUS") { + shardSocket.send(JSON.stringify({ + op: GatewayOpcode.StatusUpdate, + d: { + since: null, + game: message.data.game.name + ? { + name: message.data.game.name, + type: message.data.game.type, + } + : null, + status: message.data.status, + afk: false, + }, + })); + } }; } diff --git a/module/shardingManager.ts b/module/shardingManager.ts index 03023c7d4..756e3e9f4 100644 --- a/module/shardingManager.ts +++ b/module/shardingManager.ts @@ -554,3 +554,12 @@ export function requestAllMembers( options, }); } + +export function sendGatewayCommand(type: "EDIT_BOTS_STATUS", payload: object) { + shards.forEach((shard) => { + shard.postMessage({ + type, + ...payload, + }); + }); +} diff --git a/types/activity.ts b/types/activity.ts index 06a79fe70..937772fed 100644 --- a/types/activity.ts +++ b/types/activity.ts @@ -10,8 +10,12 @@ export interface ActivityPayload { } export enum ActivityType { + /** Example: "Playing Rocket League" */ Game, + /** Example: "Streaming Rocket League" */ Streaming, + /** Example: "Listening to spotify" */ Listening, + /** Example: ":smiley: I am cool" */ Custom = 4, } diff --git a/types/guild.ts b/types/guild.ts index 0acc33dee..a427c3a09 100644 --- a/types/guild.ts +++ b/types/guild.ts @@ -1,307 +1,307 @@ -import { Emoji, StatusType } from "./discord.ts" -import { User } from "../structures/user.ts" -import { Permission } from "./permission.ts" -import { RoleData } from "./role.ts" -import { MemberCreatePayload } from "./member.ts" -import { Activity } from "./message.ts" -import { Client_Status_Payload } from "./presence.ts" -import { ChannelCreatePayload } from "./channel.ts" +import { Emoji, StatusType } from "./discord.ts"; +import { User } from "../structures/user.ts"; +import { Permission } from "./permission.ts"; +import { RoleData } from "./role.ts"; +import { MemberCreatePayload } from "./member.ts"; +import { Activity } from "./message.ts"; +import { Client_Status_Payload } from "./presence.ts"; +import { ChannelCreatePayload } from "./channel.ts"; export interface GuildRolePayload { /** The id of the guild */ - guild_id: string + guild_id: string; /** The role object of the role created, deleted, or updated */ - role: RoleData + role: RoleData; } export interface GuildMemberChunkPayload { /** The id of the guild */ - guild_id: string + guild_id: string; /** The set of guild members */ - members: MemberCreatePayload[] + members: MemberCreatePayload[]; /** The chunk index in the expected chunks for this response */ - chunk_index: number + chunk_index: number; /** The total number of expected chunks for this response */ - chunk_count: number + chunk_count: number; /** if passing an invalid id, it will be found here */ - not_found?: string[] + not_found?: string[]; /** if passing true, presences of the members will be here */ - presences?: Presence[] + presences?: Presence[]; /** The nonce to help identify */ - nonce?: string + nonce?: string; } export interface GuildMemberUpdatePayload { /** The id of the guild */ - guild_id: string + guild_id: string; /** The user's role ids */ - roles: string[] + roles: string[]; /** The user */ - user: UserPayload + user: UserPayload; /** The nickname of the user in the guild */ - nick: string + nick: string; /** When the user used their nitro boost on the guild. */ - premium_since: string | null + premium_since: string | null; } export interface GuildMemberAddPayload extends MemberCreatePayload { - guild_id: string + guild_id: string; } export interface GuildEmojisUpdatePayload { - guild_id: string - emojis: Emoji[] + guild_id: string; + emojis: Emoji[]; } export interface GuildBanPayload { /** The id of the guild */ - guild_id: string + guild_id: string; /** The banned user. Not a member as you can ban users outside of your guild. */ - user: UserPayload + user: UserPayload; } export interface GuildDeletePayload { /** The id of the guild */ - id: string + id: string; /** Whether this guild went unavailable. */ - unavailable?: boolean + unavailable?: boolean; } export interface CreateGuildPayload { /** The guild id */ - id: string + id: string; /** The guild name 2-100 characters */ - name: string + name: string; /** The guild icon image hash */ - icon: string | null + icon: string | null; /** The guild splash image hash */ - splash: string | null + splash: string | null; /** The id of the owner */ - owner_id: string + owner_id: string; /** The voice region id for the guild */ - region: string + region: string; /** The afk channel id */ - afk_channel_id: string | null + afk_channel_id: string | null; /** AFK Timeout in seconds. */ - afk_timeout: number + afk_timeout: number; /** Whether this guild is embeddable (widget) */ - embed_enabled?: boolean + embed_enabled?: boolean; /** If not null, the channel id that the widge will generate an invite to. */ - embed_channel_id?: string | null + embed_channel_id?: string | null; /** The verification level required for the guild */ - verification_level: number + verification_level: number; /** The roles in the guild */ - roles: RoleData[] + roles: RoleData[]; /** The custom guild emojis */ - emojis: Emoji[] + emojis: Emoji[]; /** Enabled guild features */ - features: Guild_Features[] + features: Guild_Features[]; /** Required MFA level for the guild */ - mfa_level: number + mfa_level: number; /** The id of the channel to which system mesages are sent */ - system_channel_id: string | null + system_channel_id: string | null; /** When this guild was joined at */ - joined_at: string + joined_at: string; /** Whether this is considered a large guild */ - large: boolean + large: boolean; /** Whether this guild is unavailable */ - unavailable: boolean + unavailable: boolean; /** Total number of members in this guild */ - member_count?: number - voice_states: Voice_State[] + member_count?: number; + voice_states: Voice_State[]; /** Users in the guild */ - members: MemberCreatePayload[] + members: MemberCreatePayload[]; /** Channels in the guild */ - channels: ChannelCreatePayload[] - presences: Presence[] + channels: ChannelCreatePayload[]; + presences: Presence[]; /** The maximum amount of presences for the guild(the default value, currently 5000 is in effect when null is returned.) */ - max_presences?: number | null + max_presences?: number | null; /** The maximum amount of members for the guild */ - max_members?: number + max_members?: number; /** The vanity url code for the guild */ - vanity_url_code: string | null + vanity_url_code: string | null; /** The description for the guild */ - description: string | null + description: string | null; /** The banner hash */ - banner: string | null + banner: string | null; /** The premium tier */ - premium_tier: number + premium_tier: number; /** The total number of users currently boosting this server. */ - premium_subscription_count: number + premium_subscription_count: number; /** The preferred local of this guild only set if guild has the DISCOVERABLE feature, defaults to en-US */ - preferred_locale: string + preferred_locale: string; } export type Guild_Features = - | `INVITE_SPLASH` - | `VIP_REGIONS` - | `VANITY_URL` - | `VERIFIED` - | `PARTNERED` - | `PUBLIC` - | `COMMERCE` - | `NEWS` - | `DISCOVERABLE` - | `FEATURABLE` - | `ANIMATED_ICON` - | `BANNER` + | "INVITE_SPLASH" + | "VIP_REGIONS" + | "VANITY_URL" + | "VERIFIED" + | "PARTNERED" + | "PUBLIC" + | "COMMERCE" + | "NEWS" + | "DISCOVERABLE" + | "FEATURABLE" + | "ANIMATED_ICON" + | "BANNER"; export interface Voice_Region { /** unique ID for the region */ - id: string + id: string; /** name of the region */ - name: string + name: string; /** true if this is a vip-only server */ - vip: boolean + vip: boolean; /** true for a single server that is closest to the current user's client */ - optimal: boolean + optimal: boolean; /** whether this is a deprecated voice region (avoid switching to these) */ - deprecated: boolean + deprecated: boolean; /** whether this is a custom voice region (used for events/etc) */ - custom: boolean + custom: boolean; } export interface BanOptions { /** number of days to delete messages for (0-7) */ - delete_message_days?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 + delete_message_days?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; /** The reason for the ban. */ - reason?: string + reason?: string; } export interface BannedUser { /** The reason for the ban */ - reason?: string + reason?: string; /** The banned user object */ - user: User + user: User; } export interface PositionSwap { /** The unique id */ - id: string + id: string; /** The sorting position number. */ - position: number + position: number; } export interface GuildEditOptions { /** The guild name */ - name?: string + name?: string; /** The guild voice region id */ - region?: string + region?: string; /** The verification level. 0 is UNRESTRICTED. 1 is Verified email. 2 is 5 minutes user. 3 is 10 minutes member in guild. 4 is verified phone number */ - verification_level?: 0 | 1 | 2 | 3 + verification_level?: 0 | 1 | 2 | 3; /** The default message notification level. 0 is ALL_MESSAGES and 1 is ONLY_MENTINS */ - default_message_notifications?: 0 | 1 + default_message_notifications?: 0 | 1; /** Explicit content filter level. 0 is DISABLED 1 is members without roles. 2 is all members */ - explicit_content_filter?: 0 | 1 | 2 + explicit_content_filter?: 0 | 1 | 2; /** The id for the afk channel. */ - afk_channel_id?: string + afk_channel_id?: string; /** The afk timeout in seconds. */ - afk_timeout?: number + afk_timeout?: number; /** base64 1024x1024 png/jpeg/gif image for the guild icon (can be animated gif when the server has ANIMATED_ICON feature) */ - icon?: string + icon?: string; /** user id to transfer guild ownership to (must be owner) */ - owner_id?: string + owner_id?: string; /** base64 16:9 png/jpeg image for the guild splash (when the server has INVITE_SPLASH feature) */ - splash?: string + splash?: string; /** base64 16:9 png/jpeg image for the guild banner (when the server has BANNER feature) */ - banner?: string + banner?: string; /** the id of the channel to which system messages are sent */ - system_channel_id?: string + system_channel_id?: string; } export interface EditIntegrationOptions { /** The behavior when an integration subscription lapses. */ - expire_behavior: number + expire_behavior: number; /** The period in seconds where the integration will ignore lapsed subscriptions */ - expire_grace_period: number + expire_grace_period: number; /** Whether emoticons should be synced for this integrations (twitch only currently) */ - enable_emoticons: boolean + enable_emoticons: boolean; } export interface Guild_Integration { /** The integrations unique id */ - id: string + id: string; /** the integrations name */ - name: string + name: string; /** The integration type like twitch, youtube etc */ - type: string + type: string; /** Is this integration enabled */ - enabled: boolean + enabled: boolean; /** is this integration syncing */ - syncing: boolean + syncing: boolean; /** id that this integration uses for "subscribers" */ - role_id: string + role_id: string; /** The behavior of expiring subscribers */ - expire_behavior: number + expire_behavior: number; /** The grace period before expiring subscribers */ - expire_grace_period: number + expire_grace_period: number; /** The user for this integration */ - user: UserPayload + user: UserPayload; /** The integration account information */ - account: Account + account: Account; /** When this integration was last synced */ - synced_at: string + synced_at: string; } export interface Account { /** id of the account */ - id: string + id: string; /** name of the account */ - name: string + name: string; } export interface UserPayload { /** The user's id */ - id: string + id: string; /** the user's username, not unique across the platform */ - username: string + username: string; /** The user's 4 digit discord tag */ - discriminator: string + discriminator: string; /** The user's avatar hash */ - avatar: string | null + avatar: string | null; /** Whether the user is a bot */ - bot?: boolean + bot?: boolean; /** Whether the user is an official discord system user (part of the urgent message system.) */ - system?: boolean + system?: boolean; /** Whether the user has two factor enabled on their account */ - mfa_enabled?: boolean + mfa_enabled?: boolean; /** the user's chosen language option */ - locale?: string + locale?: string; /** Whether the email on this account has been verified */ - verified?: boolean + verified?: boolean; /** The user's email */ - email?: string + email?: string; /** The flags on a user's account. */ - flags?: number + flags?: number; /** The type of Nitro subscription on a user's account. */ - premium_type?: number + premium_type?: number; } export interface Partial_User { /** The user's id */ - id: string + id: string; /** the user's username, not unique across the platform */ - username?: string + username?: string; /** The user's 4 digit discord tag */ - discriminator?: string + discriminator?: string; /** The user's avatar hash */ - avatar?: string | null + avatar?: string | null; /** Whether the user is a bot */ - bot?: boolean + bot?: boolean; /** Whether the user is an official discord system user (part of the urgent message system.) */ - system?: boolean + system?: boolean; /** Whether the user has two factor enabled on their account */ - mfa_enabled?: boolean + mfa_enabled?: boolean; /** the user's chosen language option */ - locale?: string + locale?: string; /** Whether the email on this account has been verified */ - verified?: boolean + verified?: boolean; /** The user's email */ - email?: string + email?: string; /** The flags on a user's account. */ - flags?: number + flags?: number; /** The type of Nitro subscription on a user's account. */ - premium_type?: number + premium_type?: number; } export enum User_Flags { @@ -324,62 +324,62 @@ export enum Nitro_Types { } export interface Vanity_Invite { - code: string | null - uses: number + code: string | null; + uses: number; } export interface Guild_Embed { /** Whether the embed is enbaled. */ - enabled: boolean + enabled: boolean; } export interface GetAuditLogsOptions { /** Filter the logs for actions made by this user. */ - user_id?: string + user_id?: string; /** The type of audit log. */ - action_type?: AuditLogType + action_type?: AuditLogType; /** Filter the logs before a certain log entry. */ - before?: string + before?: string; /** How many entries are returned. Between 1-100. Default 50. */ - limit?: number + limit?: number; } export type AuditLogType = - | `GUILD_UPDATE` - | `CHANNEL_CREATE` - | `CHANNEL_UPDATE` - | `CHANNEL_DELETE` - | `CHANNEL_OVERWRITE_CREATE` - | `CHANNEL_OVERWRITE_UPDATE` - | `CHANNEL_OVERWRITE_DELETE` - | `MEMBER_KICK` - | `MEMBER_PRUNE` - | `MEMBER_BAN_ADD` - | `MEMBER_BAN_REMOVE` - | `MEMBER_UPDATE` - | `MEMBER_ROLE_UPDATE` - | `MEMBER_MOVE` - | `MEMBER_DISCONNECT` - | `BOT_ADD` - | `ROLE_CREATE` - | `ROLE_UPDATE` - | `ROLE_DELETE` - | `INVITE_CREATE` - | `INVITE_UPDATE` - | `INVITE_DELETE` - | `WEBHOOK_CREATE` - | `WEBHOOK_UPDATE` - | `WEBHOOK_DELETE` - | `EMOJI_CREATE` - | `EMOJI_UPDATE` - | `EMOJI_DELETE` - | `MESSAGE_DELETE` - | `MESSAGE_BULK_DELETE` - | `MESSAGE_PIN` - | `MESSAGE_UNPIN` - | `INTEGRATION_CREATE` - | `INTEGRATION_UPDATE` - | `INTEGRATION_DELETE` + | "GUILD_UPDATE" + | "CHANNEL_CREATE" + | "CHANNEL_UPDATE" + | "CHANNEL_DELETE" + | "CHANNEL_OVERWRITE_CREATE" + | "CHANNEL_OVERWRITE_UPDATE" + | "CHANNEL_OVERWRITE_DELETE" + | "MEMBER_KICK" + | "MEMBER_PRUNE" + | "MEMBER_BAN_ADD" + | "MEMBER_BAN_REMOVE" + | "MEMBER_UPDATE" + | "MEMBER_ROLE_UPDATE" + | "MEMBER_MOVE" + | "MEMBER_DISCONNECT" + | "BOT_ADD" + | "ROLE_CREATE" + | "ROLE_UPDATE" + | "ROLE_DELETE" + | "INVITE_CREATE" + | "INVITE_UPDATE" + | "INVITE_DELETE" + | "WEBHOOK_CREATE" + | "WEBHOOK_UPDATE" + | "WEBHOOK_DELETE" + | "EMOJI_CREATE" + | "EMOJI_UPDATE" + | "EMOJI_DELETE" + | "MESSAGE_DELETE" + | "MESSAGE_BULK_DELETE" + | "MESSAGE_PIN" + | "MESSAGE_UNPIN" + | "INTEGRATION_CREATE" + | "INTEGRATION_UPDATE" + | "INTEGRATION_DELETE"; export enum AuditLogs { GUILD_UPDATE = 1, @@ -419,129 +419,134 @@ export enum AuditLogs { INTEGRATION_DELETE, } -export type ChannelType = "text" | "dm" | "news" | "voice" | "category" | "store" +export type ChannelType = + | "text" + | "dm" + | "news" + | "voice" + | "category" + | "store"; export interface Overwrite { /** The role or user id */ - id: string + id: string; /** Whether this is a role or a member */ - type: "role" | "member" + type: "role" | "member"; /** The permissions that this id is allowed to do. (This will mark it as a green check.) */ - allow: Permission[] + allow: Permission[]; /** The permissions that this id is NOT allowed to do. (This will mark it as a red x.) */ - deny: Permission[] + deny: Permission[]; } export interface Raw_Overwrite { /** The role or user id */ - id: string + id: string; /** Whether this is a role or a member */ - type: "role" | "member" + type: "role" | "member"; /** The permissions that this id is allowed to do. (This will mark it as a green check.) */ - allow: number + allow: number; /** The permissions that this id is NOT allowed to do. (This will mark it as a red x.) */ - deny: number + deny: number; } export interface ChannelCreate_Options { /** The type of the channel */ - type?: ChannelType + type?: ChannelType; /** The channel topic. (0-1024 characters) */ - topic?: string + topic?: string; /** The bitrate(in bits) of the voice channel. */ - bitrate?: number + bitrate?: number; /** The user limit of the voice channel. */ - user_limit?: number + user_limit?: number; /** The amount of seconds a user has to wait before sending another message. (0-21600 seconds). Bots, as well as users with the permission `manage_messages or manage_channel` are unaffected. */ - rate_limit_per_user?: number + rate_limit_per_user?: number; /** The sorting position of the channel */ - position?: number + position?: number; /** The channel's permission overwrites */ - permission_overwrites?: Overwrite[] + permission_overwrites?: Overwrite[]; /** The id of the parent category for the channel */ - parent_id?: string + parent_id?: string; /** Whether the channel is nsfw */ - nsfw?: boolean + nsfw?: boolean; /** The reason to add in the Audit Logs. */ - reason?: string + reason?: string; } export interface CreateEmojisOptions { /** The roles for which this emoji will be whitelisted. Only the users with one of these roles can use this emoji. */ - roles: string[] + roles: string[]; /** The reason to have in the Audit Logs. */ - reason: string + reason: string; } export interface EditEmojisOptions { /** The name of the emoji */ - name: string + name: string; /** The roles for which this emoji will be whitelisted. Only the users with one of these roles can use this emoji. */ - roles: string[] + roles: string[]; } export interface CreateRoleOptions { - name?: string - permissions?: Permission[] - color?: number - hoist?: boolean - mentionable?: boolean + name?: string; + permissions?: Permission[]; + color?: number; + hoist?: boolean; + mentionable?: boolean; } export interface PrunePayload { - pruned: number + pruned: number; } export interface Voice_State { /** the guild id this voice state is for */ - guild_id?: string + guild_id?: string; /** the channel id this user is connected to */ - channel_id: string | null + channel_id: string | null; /** the user id this voice state is for */ - user_id: string + user_id: string; /** the guild member this voice state is for */ - member?: MemberCreatePayload + member?: MemberCreatePayload; /** the session id for this voice state */ - session_id: string + session_id: string; /** whether this user is deafened by the server */ - deaf: boolean + deaf: boolean; /** whether this user is muted by the server */ - mute: boolean + mute: boolean; /** whether this user is locally deafened */ - self_deaf: boolean + self_deaf: boolean; /** whether this user is locally muted */ - self_mute: boolean + self_mute: boolean; /** whether this user is streaming using "Go Live" */ - self_stream?: boolean + self_stream?: boolean; /** whether this user is muted by the current user */ - suppress: boolean + suppress: boolean; } export interface Presence { /** The user presence is being updated for */ - user: UserPayload + user: UserPayload; /** The roles this user is in */ - roles: string[] + roles: string[]; /** null, or the user's current activity */ - game: Activity | null + game: Activity | null; /** The id of the guild */ - guild_id: string + guild_id: string; /** Either idle */ - status: StatusType - activities: Activity[] - client_status: Client_Status_Payload - premium_since?: string | null - nick?: string | null + status: StatusType; + activities: Activity[]; + client_status: Client_Status_Payload; + premium_since?: string | null; + nick?: string | null; } export interface FetchMembersOptions { /** Used to specify if you want the presences of the matched members. Default = false. */ - presences?: boolean + presences?: boolean; /** Only returns members whose username or nickname starts with this string. DO NOT INCLUDE discriminators. If a string is provided, the max amount of members that can be fetched is 100. Default = return all members. */ - query?: string + query?: string; /** Used to specify which users to fetch specifically. */ - userIDs?: string[] + userIDs?: string[]; /** Maximum number of members to return that match the query. Default = 0 which will return all members. */ - limit?: number - + limit?: number; } diff --git a/types/permission.ts b/types/permission.ts index 559e880b6..7c2a896ba 100644 --- a/types/permission.ts +++ b/types/permission.ts @@ -1,34 +1,34 @@ export type Permission = - | `CREATE_INSTANT_INVITE` - | `KICK_MEMBERS` - | `BAN_MEMBERS` - | `ADMINISTRATOR` - | `MANAGE_CHANNELS` - | `MANAGE_GUILD` - | `ADD_REACTIONS` - | `VIEW_AUDIT_LOG` - | `VIEW_CHANNEL` - | `SEND_MESSAGES` - | `SEND_TTS_MESSAGES` - | `MANAGE_MESSAGES` - | `EMBED_LINKS` - | `ATTACH_FILES` - | `READ_MESSAGE_HISTORY` - | `MENTION_EVERYONE` - | `USE_EXTERNAL_EMOJIS` - | `CONNECT` - | `SPEAK` - | `MUTE_MEMBERS` - | `DEAFEN_MEMBERS` - | `MOVE_MEMBERS` - | `USE_VAD` - | `PRIORITY_SPEAKER` - | `STREAM` - | `CHANGE_NICKNAME` - | `MANAGE_NICKNAMES` - | `MANAGE_ROLES` - | `MANAGE_WEBHOOKS` - | `MANAGE_EMOJIS` + | "CREATE_INSTANT_INVITE" + | "KICK_MEMBERS" + | "BAN_MEMBERS" + | "ADMINISTRATOR" + | "MANAGE_CHANNELS" + | "MANAGE_GUILD" + | "ADD_REACTIONS" + | "VIEW_AUDIT_LOG" + | "VIEW_CHANNEL" + | "SEND_MESSAGES" + | "SEND_TTS_MESSAGES" + | "MANAGE_MESSAGES" + | "EMBED_LINKS" + | "ATTACH_FILES" + | "READ_MESSAGE_HISTORY" + | "MENTION_EVERYONE" + | "USE_EXTERNAL_EMOJIS" + | "CONNECT" + | "SPEAK" + | "MUTE_MEMBERS" + | "DEAFEN_MEMBERS" + | "MOVE_MEMBERS" + | "USE_VAD" + | "PRIORITY_SPEAKER" + | "STREAM" + | "CHANGE_NICKNAME" + | "MANAGE_NICKNAMES" + | "MANAGE_ROLES" + | "MANAGE_WEBHOOKS" + | "MANAGE_EMOJIS"; export enum Permissions { CREATE_INSTANT_INVITE = 0x00000001, @@ -60,5 +60,5 @@ export enum Permissions { MANAGE_NICKNAMES = 0x08000000, MANAGE_ROLES = 0x10000000, MANAGE_WEBHOOKS = 0x20000000, - MANAGE_EMOJIS = 0x40000000 + MANAGE_EMOJIS = 0x40000000, } diff --git a/utils/utils.ts b/utils/utils.ts index 8a8048c94..6dd3098a0 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -1,3 +1,15 @@ +import { StatusType } from "../types/discord.ts"; +import { ActivityType } from "../types/activity.ts"; +import { sendGatewayCommand } from "../module/shardingManager.ts"; + export const sleep = (timeout: number) => { return new Promise((resolve) => setTimeout(resolve, timeout)); }; + +export function editBotsStatus( + status: StatusType, + name?: string, + type = ActivityType.Game, +) { + sendGatewayCommand("EDIT_BOTS_STATUS", { status, game: { name, type } }); +} From 224771e550f762058f65523d991e8ecb4f23af48 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Sat, 16 May 2020 10:41:03 -0400 Subject: [PATCH 17/37] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7b50c0324..0930b7703 100644 --- a/README.md +++ b/README.md @@ -304,3 +304,9 @@ This section will list out all the available methods and functionality in the li ```ts - .avatarURL(size, format) ``` + +## Utils + +```ts +.editBotsStatus(status, name, type) +``` From 68316ffaf3873fc5b13ddec8337c924866e8ffff Mon Sep 17 00:00:00 2001 From: Skillz Date: Sat, 16 May 2020 13:21:58 -0400 Subject: [PATCH 18/37] fix resume crashes --- module/gateway.ts | 22 ---------- module/requestManager.ts | 2 +- module/shard.ts | 90 ++++++++++++++++++++++----------------- module/shardingManager.ts | 5 --- 4 files changed, 53 insertions(+), 66 deletions(-) delete mode 100644 module/gateway.ts diff --git a/module/gateway.ts b/module/gateway.ts deleted file mode 100644 index b59cefde2..000000000 --- a/module/gateway.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { WebSocket } from "https://deno.land/std@0.50.0/ws/mod.ts"; -import { GatewayOpcode } from "../types/discord.ts"; -import { delay } from "https://deno.land/std@0.50.0/async/mod.ts"; - -// Discord requests null if no number has yet been sent by discord -export let previousSequenceNumber: number | null = null; - -// TODO: If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume. -export const sendConstantHeartbeats = async ( - socket: WebSocket, - interval: number, -) => { - await delay(interval); - socket.send( - JSON.stringify({ op: GatewayOpcode.Heartbeat, d: previousSequenceNumber }), - ); - sendConstantHeartbeats(socket, interval); -}; - -export const updatePreviousSequenceNumber = (sequence: number) => { - previousSequenceNumber = sequence; -}; diff --git a/module/requestManager.ts b/module/requestManager.ts index edbb4fece..15ef68ce2 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -125,7 +125,7 @@ async function runMethod( }); } -function handleStatusCode(status: number): boolean { +function handleStatusCode(status: number) { if (status >= 200 && status < 400) { return true; } diff --git a/module/shard.ts b/module/shard.ts index 376e7fe2e..925e60ed8 100644 --- a/module/shard.ts +++ b/module/shard.ts @@ -10,45 +10,62 @@ import { ReadyPayload, } from "../types/discord.ts"; import { logRed } from "../utils/logger.ts"; -import { sendConstantHeartbeats, previousSequenceNumber } from "./gateway.ts"; import { FetchMembersOptions } from "../types/guild.ts"; - +import { delay } from "https://deno.land/std@0.50.0/async/delay.ts"; let shardSocket: WebSocket; /** The session id is needed for RESUME functionality when discord disconnects randomly. */ let sessionID = ""; -async function resumeConnection( - payload: object, - botGatewayData: DiscordBotGatewayData, - socket: WebSocket, +// Discord requests null if no number has yet been sent by discord +export let previousSequenceNumber: number | null = null; +let needToResume = false; + +// TODO: If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume. +async function sendConstantHeartbeats( + interval: number, ) { - return setInterval(async () => { - socket = await connectWebSocket(botGatewayData.url); - await socket.send( - JSON.stringify({ - op: GatewayOpcode.Resume, - d: { - ...payload, - session_id: sessionID, - seq: previousSequenceNumber, - }, - }), - ); - }, 1000 * 15); + await delay(interval); + shardSocket.send( + JSON.stringify({ op: GatewayOpcode.Heartbeat, d: previousSequenceNumber }), + ); + sendConstantHeartbeats(interval); +} + +async function resumeConnection( + botGatewayData: DiscordBotGatewayData, + identifyPayload: object, +) { + // Run it once + createShard(botGatewayData, identifyPayload, true); + // Then retry every 15 seconds + await delay(1000 * 15); + if (needToResume) resumeConnection(botGatewayData, identifyPayload); } export const createShard = async ( botGatewayData: DiscordBotGatewayData, identifyPayload: object, + resuming = false, ) => { shardSocket = await connectWebSocket(botGatewayData.url); let resumeInterval = 0; - // Intial identify with the gateway - await shardSocket.send( - JSON.stringify({ op: GatewayOpcode.Identify, d: identifyPayload }), - ); + if (!resuming) { + // Intial identify with the gateway + await shardSocket.send( + JSON.stringify({ op: GatewayOpcode.Identify, d: identifyPayload }), + ); + } else { + await shardSocket.send(JSON.stringify({ + op: GatewayOpcode.Resume, + d: { + ...identifyPayload, + session_id: sessionID, + seq: previousSequenceNumber, + }, + })); + } try { for await (const message of shardSocket) { @@ -58,27 +75,27 @@ export const createShard = async ( switch (data.op) { case GatewayOpcode.Hello: sendConstantHeartbeats( - shardSocket, (data.d as DiscordHeartbeatPayload).heartbeat_interval, ); break; case GatewayOpcode.Reconnect: case GatewayOpcode.InvalidSession: - // Reconnect to the gateway https://discordapp.com/developers/docs/topics/gateway#reconnect - resumeInterval = await resumeConnection( - identifyPayload, - botGatewayData, - shardSocket, - ); - break; - case GatewayOpcode.Resume: - clearInterval(resumeInterval); + needToResume = true; + resumeConnection(botGatewayData, identifyPayload); break; default: + if (data.t === "RESUMED") { + needToResume = false; + break; + } // Important for RESUME if (data.t === "READY") { sessionID = (data.d as ReadyPayload).session_id; } + + // Update the sequence number if it is present + if (data.s) previousSequenceNumber = data.s; + // @ts-ignore postMessage( { @@ -91,11 +108,8 @@ export const createShard = async ( } } else if (isWebSocketCloseEvent(message)) { logRed(`Close :( ${JSON.stringify(message)}`); - resumeInterval = await resumeConnection( - identifyPayload, - botGatewayData, - shardSocket, - ); + needToResume = true; + resumeConnection(botGatewayData, identifyPayload); } } } catch (error) { diff --git a/module/shardingManager.ts b/module/shardingManager.ts index 756e3e9f4..c805cd1f8 100644 --- a/module/shardingManager.ts +++ b/module/shardingManager.ts @@ -14,9 +14,6 @@ import { botID, } from "./client.ts"; import { delay } from "https://deno.land/std@0.50.0/async/delay.ts"; -import { - updatePreviousSequenceNumber, -} from "./gateway.ts"; import { handleInternalChannelCreate, handleInternalChannelUpdate, @@ -105,8 +102,6 @@ export const spawnShards = async ( }; function handleDiscordPayload(data: DiscordPayload) { - // Update the sequence number if it is present - if (data.s) updatePreviousSequenceNumber(data.s); eventHandlers.raw?.(data); switch (data.op) { From 4ca6041b3d9edc38699ebb67b57094cf84403d56 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Sat, 16 May 2020 13:45:00 -0400 Subject: [PATCH 19/37] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0930b7703..1795979cd 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,15 @@ Discord API library wrapper in Deno If you are just starting out, you can use the Discordeno Template repo to get the base of your bot pre-built for you. As other developers create other command frameworks for this library, those frameworks will be listed here: -**[Official Boilerplate](https://github.com/Skillz4Killz/Discordeno-bot-template):** This is a very minimalistic design for a boilerplate for your bot to get you started. +| Bot Name | Developer | Links | Description | +|--------------------|--------------------|---------------------------------------------------------|-----| +| Official Boilerplate | Skillz4Killz#4500 | [Github](https://github.com/Skillz4Killz/Discordeno-bot-template), [Support Server](https://discord.gg/J4NqJ72) | This is a very minimalistic design for a boilerplate for your bot to get you started. | +| DenoBot | NTM Nathan#0001 | [Github](https://github.com/ntm-development/DenoBot), [Support Server](https://discord.com/invite/G2rb53z) | | ## Open Source Bots Using Discordeno | Bot Name | Developer | Links | |--------------------|--------------------|---------------------------------------------------------| -| DenoBot | NTM Nathan#0001 | [Github](https://github.com/ntm-development/DenoBot), [Support Server](https://discord.com/invite/G2rb53z) | | discordeno-mattis | Mattis6666 | [Github](https://github.com/Mattis6666/discordeno-mattis/) | ## Motivations/Features From 15c682310e609b1e99b01e735f69a17932e7825d Mon Sep 17 00:00:00 2001 From: Skillz Date: Sat, 16 May 2020 13:54:44 -0400 Subject: [PATCH 20/37] remove old comment --- module/requestManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/module/requestManager.ts b/module/requestManager.ts index 15ef68ce2..e4752119a 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -39,7 +39,6 @@ async function processQueue() { processRateLimitedPaths(); export const RequestManager = { - // Something off about using runMethod with get breaks when using fetch get: async (url: string, body?: unknown) => { return runMethod(RequestMethod.Get, url, body); }, From d0f0e87416d654075dfef017152828700bf55794 Mon Sep 17 00:00:00 2001 From: NTM Nathan <34463340+NTMNathan@users.noreply.github.com> Date: Sun, 17 May 2020 12:13:22 +1000 Subject: [PATCH 21/37] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1795979cd..d244836b5 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ If you are just starting out, you can use the Discordeno Template repo to get th | Bot Name | Developer | Links | Description | |--------------------|--------------------|---------------------------------------------------------|-----| | Official Boilerplate | Skillz4Killz#4500 | [Github](https://github.com/Skillz4Killz/Discordeno-bot-template), [Support Server](https://discord.gg/J4NqJ72) | This is a very minimalistic design for a boilerplate for your bot to get you started. | -| DenoBot | NTM Nathan#0001 | [Github](https://github.com/ntm-development/DenoBot), [Support Server](https://discord.com/invite/G2rb53z) | | +| DenoBot | NTM Nathan#0001 | [Github](https://github.com/ntm-development/DenoBot), [Support Server](https://discord.com/invite/G2rb53z) | Another boilerplate example of the first one, with more commands and improvements. | ## Open Source Bots Using Discordeno From ec63129f3d3a680f6afa2277b615e7eb714345b3 Mon Sep 17 00:00:00 2001 From: Skillz Date: Sun, 17 May 2020 08:11:51 -0400 Subject: [PATCH 22/37] update channel cache on channel events --- events/channels.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/events/channels.ts b/events/channels.ts index c2b73f56d..03d2675ca 100644 --- a/events/channels.ts +++ b/events/channels.ts @@ -6,6 +6,12 @@ import { eventHandlers } from "../module/client.ts"; export const handleInternalChannelCreate = (data: ChannelCreatePayload) => { const channel = createChannel(data); cache.channels.set(channel.id, channel); + if (channel.guild_id) { + const guild = cache.guilds.get(channel.guild_id); + if (guild) { + guild.channels.set(channel.id, channel); + } + } eventHandlers.channelCreate?.(channel); }; @@ -15,6 +21,13 @@ export const handleInternalChannelUpdate = (data: ChannelCreatePayload) => { cache.channels.set(channel.id, channel); if (!cachedChannel) return; + if (channel.guild_id) { + const guild = cache.guilds.get(channel.guild_id) + if (guild) { + guild.channels.set(channel.id, channel) + } + } + eventHandlers.channel_update?.(channel, cachedChannel); }; @@ -42,6 +55,8 @@ export const handleInternalChannelDelete = (data: ChannelCreatePayload) => { ], }); } + + guild?.channels.delete(data.id) } cache.channels.delete(data.id); From b18df95050691e0846cd1a023c2de480c8eef47e Mon Sep 17 00:00:00 2001 From: Skillz Date: Sun, 17 May 2020 09:05:46 -0400 Subject: [PATCH 23/37] fmt --- events/channels.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/events/channels.ts b/events/channels.ts index 03d2675ca..642156c83 100644 --- a/events/channels.ts +++ b/events/channels.ts @@ -22,9 +22,9 @@ export const handleInternalChannelUpdate = (data: ChannelCreatePayload) => { if (!cachedChannel) return; if (channel.guild_id) { - const guild = cache.guilds.get(channel.guild_id) + const guild = cache.guilds.get(channel.guild_id); if (guild) { - guild.channels.set(channel.id, channel) + guild.channels.set(channel.id, channel); } } @@ -56,7 +56,7 @@ export const handleInternalChannelDelete = (data: ChannelCreatePayload) => { }); } - guild?.channels.delete(data.id) + guild?.channels.delete(data.id); } cache.channels.delete(data.id); From 32e07422f314393298ef0c0ed3de4035b64697bd Mon Sep 17 00:00:00 2001 From: Skillz Date: Sun, 17 May 2020 09:28:39 -0400 Subject: [PATCH 24/37] bette error handling --- module/shard.ts | 165 ++++++++++++++++++++++++------------------------ 1 file changed, 83 insertions(+), 82 deletions(-) diff --git a/module/shard.ts b/module/shard.ts index 925e60ed8..60ba65cb3 100644 --- a/module/shard.ts +++ b/module/shard.ts @@ -12,13 +12,14 @@ import { import { logRed } from "../utils/logger.ts"; import { FetchMembersOptions } from "../types/guild.ts"; import { delay } from "https://deno.land/std@0.50.0/async/delay.ts"; + let shardSocket: WebSocket; /** The session id is needed for RESUME functionality when discord disconnects randomly. */ let sessionID = ""; // Discord requests null if no number has yet been sent by discord -export let previousSequenceNumber: number | null = null; +let previousSequenceNumber: number | null = null; let needToResume = false; // TODO: If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume. @@ -43,7 +44,7 @@ async function resumeConnection( if (needToResume) resumeConnection(botGatewayData, identifyPayload); } -export const createShard = async ( +const createShard = async ( botGatewayData: DiscordBotGatewayData, identifyPayload: object, resuming = false, @@ -67,57 +68,64 @@ export const createShard = async ( })); } - try { - for await (const message of shardSocket) { - if (typeof message === "string") { - const data = JSON.parse(message); + for await (const message of shardSocket) { + if (typeof message === "string") { + const data = JSON.parse(message); - switch (data.op) { - case GatewayOpcode.Hello: - sendConstantHeartbeats( - (data.d as DiscordHeartbeatPayload).heartbeat_interval, - ); + switch (data.op) { + case GatewayOpcode.Hello: + sendConstantHeartbeats( + (data.d as DiscordHeartbeatPayload).heartbeat_interval, + ); + break; + case GatewayOpcode.Reconnect: + case GatewayOpcode.InvalidSession: + needToResume = true; + resumeConnection(botGatewayData, identifyPayload); + break; + default: + if (data.t === "RESUMED") { + needToResume = false; break; - case GatewayOpcode.Reconnect: - case GatewayOpcode.InvalidSession: - needToResume = true; - resumeConnection(botGatewayData, identifyPayload); - break; - default: - if (data.t === "RESUMED") { - needToResume = false; - break; - } - // Important for RESUME - if (data.t === "READY") { - sessionID = (data.d as ReadyPayload).session_id; - } + } + // Important for RESUME + if (data.t === "READY") { + sessionID = (data.d as ReadyPayload).session_id; + } - // Update the sequence number if it is present - if (data.s) previousSequenceNumber = data.s; + // Update the sequence number if it is present + if (data.s) previousSequenceNumber = data.s; - // @ts-ignore - postMessage( - { - type: "HANDLE_DISCORD_PAYLOAD", - payload: message, - resumeInterval, - }, - ); - break; - } - } else if (isWebSocketCloseEvent(message)) { - logRed(`Close :( ${JSON.stringify(message)}`); + // @ts-ignore + postMessage( + { + type: "HANDLE_DISCORD_PAYLOAD", + payload: message, + resumeInterval, + }, + ); + break; + } + } else if (isWebSocketCloseEvent(message)) { + logRed(`Close :( ${JSON.stringify(message)}`); + // These error codes should just crash the projects + if ([4004, 4005, 4012, 4013, 4014].includes(message.code)) { + throw new Error( + "Shard.ts: Error occurred that is not resumeable or able to be reconnected.", + ); + } + // These error codes can not be resumed but need to reconnect from start + if ([4003, 4007, 4008, 4009].includes(message.code)) { + createShard(botGatewayData, identifyPayload); + } else { needToResume = true; resumeConnection(botGatewayData, identifyPayload); } } - } catch (error) { - logRed(error); } }; -export function requestGuildMembers( +function requestGuildMembers( guildID: string, nonce: string, options?: FetchMembersOptions, @@ -135,46 +143,39 @@ export function requestGuildMembers( })); } -// TODO: Remove ts-ignore once https://github.com/denoland/deno/issues/5262 fixed +// TODO: Errors need to be fixed by VSC plugin +postMessage({ type: "REQUEST_CLIENT_OPTIONS" }); // @ts-ignore -if (typeof self.postMessage === "function") { - // @ts-ignore - postMessage({ type: "REQUEST_CLIENT_OPTIONS" }); -} -// @ts-ignore -if (typeof self.onmessage === "function") { - // @ts-ignore - onmessage = (message) => { - if (message.data.type === "CREATE_SHARD") { - createShard( - message.data.botGatewayData, - message.data.identifyPayload, - ); - } +onmessage = (message: MessageEvent) => { + if (message.data.type === "CREATE_SHARD") { + createShard( + message.data.botGatewayData, + message.data.identifyPayload, + ); + } - if (message.data.type === "FETCH_MEMBERS") { - requestGuildMembers( - message.data.guildID, - message.data.nonce, - message.data.options, - ); - } + if (message.data.type === "FETCH_MEMBERS") { + requestGuildMembers( + message.data.guildID, + message.data.nonce, + message.data.options, + ); + } - if (message.data.type === "EDIT_BOTS_STATUS") { - shardSocket.send(JSON.stringify({ - op: GatewayOpcode.StatusUpdate, - d: { - since: null, - game: message.data.game.name - ? { - name: message.data.game.name, - type: message.data.game.type, - } - : null, - status: message.data.status, - afk: false, - }, - })); - } - }; -} + if (message.data.type === "EDIT_BOTS_STATUS") { + shardSocket.send(JSON.stringify({ + op: GatewayOpcode.StatusUpdate, + d: { + since: null, + game: message.data.game.name + ? { + name: message.data.game.name, + type: message.data.game.type, + } + : null, + status: message.data.status, + afk: false, + }, + })); + } +}; From 16ac8b176ed4bd0a2d74f460f3769fb6f51cb5c2 Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 08:11:29 -0400 Subject: [PATCH 25/37] reload events handlers --- README.md | 1 + module/client.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index d244836b5..df676d9b8 100644 --- a/README.md +++ b/README.md @@ -311,4 +311,5 @@ This section will list out all the available methods and functionality in the li ```ts .editBotsStatus(status, name, type) +updateEventHandlers(eventHandlers) ``` diff --git a/module/client.ts b/module/client.ts index 88e2cc0c5..1184f13c5 100644 --- a/module/client.ts +++ b/module/client.ts @@ -50,3 +50,7 @@ export default createClient; export const updateChannelCache = (key: string, value: Channel) => { cache.channels.set(key, value); }; + +export function updateEventHandlers(newEventHandlers: EventHandlers) { + eventHandlers = newEventHandlers +} From 8f7071ca428cc4023da7ce382a65ecd40f47156c Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 08:15:45 -0400 Subject: [PATCH 26/37] fmt --- module/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/client.ts b/module/client.ts index 1184f13c5..72ac230c7 100644 --- a/module/client.ts +++ b/module/client.ts @@ -52,5 +52,5 @@ export const updateChannelCache = (key: string, value: Channel) => { }; export function updateEventHandlers(newEventHandlers: EventHandlers) { - eventHandlers = newEventHandlers + eventHandlers = newEventHandlers; } From 585c721b71e1698226f77142d27d82a9ae496b87 Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 08:27:16 -0400 Subject: [PATCH 27/37] fix gha --- configs.ts.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configs.ts.example b/configs.ts.example index ccb90b6ee..c5eba43ea 100644 --- a/configs.ts.example +++ b/configs.ts.example @@ -1,3 +1,3 @@ export const configs = { - token: 'BOT TOKEN HERE' -} + token: 'BOT TOKEN HERE', +}; From 51a61cf2d7afa20d9be32b3e642de4088fc412fe Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 08:31:06 -0400 Subject: [PATCH 28/37] move files to .github folder --- CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md => .github/CONTRIBUTING.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename CODE_OF_CONDUCT.md => .github/CODE_OF_CONDUCT.md (100%) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (100%) diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md From 9ece256215bcea0a5a3e23f76dc4bdd4d483c44d Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 14:03:16 -0400 Subject: [PATCH 29/37] change mod.ts to debug.ts --- mod.ts => debug.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mod.ts => debug.ts (100%) diff --git a/mod.ts b/debug.ts similarity index 100% rename from mod.ts rename to debug.ts From eef972aeec973e2b2671dbeec0714a2a05e3d216 Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 14:21:20 -0400 Subject: [PATCH 30/37] fix small typo on name --- types/message.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/message.ts b/types/message.ts index 031473837..0178158ca 100644 --- a/types/message.ts +++ b/types/message.ts @@ -137,7 +137,7 @@ export interface Reaction { /** Whether the current user reacted using this emoji */ me: boolean; /** The emoji information. Can be partial. */ - emoji: Emoji; + emoji: EmojiReaction; } export enum Message_Types { @@ -200,7 +200,7 @@ export enum Message_Flags { URGENT = 1 << 4, } -export interface Emoji { +export interface EmojiReaction { /** The emoji id. */ id?: string; /** The emoji name. Null in reaction emoji object if emoji is no longer on the server */ From e58a3224e6974a2a5897573c7bc02757df5bfa39 Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 14:22:18 -0400 Subject: [PATCH 31/37] export all from modts --- mod.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 mod.ts diff --git a/mod.ts b/mod.ts new file mode 100644 index 000000000..9a4c2a532 --- /dev/null +++ b/mod.ts @@ -0,0 +1,31 @@ +export * from './module/client.ts' +export * from './module/requestManager.ts' +export * from './module/shard.ts' +export * from './module/shardingManager.ts' + +export * from './structures/channel.ts' +export * from './structures/guild.ts' +export * from './structures/member.ts' +export * from './structures/message.ts' +export * from './structures/role.ts' +export * from './structures/user.ts' + +export * from './types/activity.ts' +export * from './types/cdn.ts' +export * from './types/channel.ts' +export * from './types/discord.ts' +export * from './types/errors.ts' +export * from './types/fetch.ts' +export * from './types/guild.ts' +export * from './types/member.ts' +export * from './types/message.ts' +export * from './types/options.ts' +export * from './types/permission.ts' +export * from './types/presence.ts' +export * from './types/role.ts' + +export * from './utils/cache.ts' +export * from './utils/cdn.ts' +export * from './utils/logger.ts' +export * from './utils/permissions.ts' +export * from './utils/utils.ts' From a98df5ebe8f7bea440dbc3825011a7564c04df53 Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 15:50:19 -0400 Subject: [PATCH 32/37] gha fix --- module/shard.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/module/shard.ts b/module/shard.ts index 60ba65cb3..128f65a69 100644 --- a/module/shard.ts +++ b/module/shard.ts @@ -144,6 +144,7 @@ function requestGuildMembers( } // TODO: Errors need to be fixed by VSC plugin +// @ts-ignore postMessage({ type: "REQUEST_CLIENT_OPTIONS" }); // @ts-ignore onmessage = (message: MessageEvent) => { From 943128ec49ddaf0382f07032bf457bcdc70cf25c Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 15:54:38 -0400 Subject: [PATCH 33/37] fmt --- mod.ts | 56 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/mod.ts b/mod.ts index 9a4c2a532..ec0348778 100644 --- a/mod.ts +++ b/mod.ts @@ -1,31 +1,31 @@ -export * from './module/client.ts' -export * from './module/requestManager.ts' -export * from './module/shard.ts' -export * from './module/shardingManager.ts' +export * from "./module/client.ts"; +export * from "./module/requestManager.ts"; +export * from "./module/shard.ts"; +export * from "./module/shardingManager.ts"; -export * from './structures/channel.ts' -export * from './structures/guild.ts' -export * from './structures/member.ts' -export * from './structures/message.ts' -export * from './structures/role.ts' -export * from './structures/user.ts' +export * from "./structures/channel.ts"; +export * from "./structures/guild.ts"; +export * from "./structures/member.ts"; +export * from "./structures/message.ts"; +export * from "./structures/role.ts"; +export * from "./structures/user.ts"; -export * from './types/activity.ts' -export * from './types/cdn.ts' -export * from './types/channel.ts' -export * from './types/discord.ts' -export * from './types/errors.ts' -export * from './types/fetch.ts' -export * from './types/guild.ts' -export * from './types/member.ts' -export * from './types/message.ts' -export * from './types/options.ts' -export * from './types/permission.ts' -export * from './types/presence.ts' -export * from './types/role.ts' +export * from "./types/activity.ts"; +export * from "./types/cdn.ts"; +export * from "./types/channel.ts"; +export * from "./types/discord.ts"; +export * from "./types/errors.ts"; +export * from "./types/fetch.ts"; +export * from "./types/guild.ts"; +export * from "./types/member.ts"; +export * from "./types/message.ts"; +export * from "./types/options.ts"; +export * from "./types/permission.ts"; +export * from "./types/presence.ts"; +export * from "./types/role.ts"; -export * from './utils/cache.ts' -export * from './utils/cdn.ts' -export * from './utils/logger.ts' -export * from './utils/permissions.ts' -export * from './utils/utils.ts' +export * from "./utils/cache.ts"; +export * from "./utils/cdn.ts"; +export * from "./utils/logger.ts"; +export * from "./utils/permissions.ts"; +export * from "./utils/utils.ts"; From 0132a3892c3b49cf81b8ab3b59e04dc25a3bd2fe Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 16:04:01 -0400 Subject: [PATCH 34/37] fix gha? --- configs.ts.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs.ts.example b/configs.ts.example index c5eba43ea..ff1ec62c4 100644 --- a/configs.ts.example +++ b/configs.ts.example @@ -1,3 +1,3 @@ export const configs = { - token: 'BOT TOKEN HERE', + token: "BOT TOKEN HERE", }; From c89a82cb706cd1043164aa1e0ce26be361a22cce Mon Sep 17 00:00:00 2001 From: Skillz Date: Mon, 18 May 2020 22:00:31 -0400 Subject: [PATCH 35/37] shard file is not public --- mod.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/mod.ts b/mod.ts index ec0348778..cb0d38e12 100644 --- a/mod.ts +++ b/mod.ts @@ -1,6 +1,5 @@ export * from "./module/client.ts"; export * from "./module/requestManager.ts"; -export * from "./module/shard.ts"; export * from "./module/shardingManager.ts"; export * from "./structures/channel.ts"; From 30b0533fb5addeb5f4ffb5271f4b349f86d0b564 Mon Sep 17 00:00:00 2001 From: Skillz Date: Tue, 19 May 2020 08:47:36 -0400 Subject: [PATCH 36/37] finalizing rate limiter --- module/requestManager.ts | 91 +++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/module/requestManager.ts b/module/requestManager.ts index e4752119a..36fdd07e7 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -4,20 +4,27 @@ import { delay } from "https://deno.land/std@0.50.0/async/delay.ts"; import { Errors } from "../types/errors.ts"; import { HttpResponseCode } from "../types/discord.ts"; -const queue: Array<() => Promise> = []; +const queue: QueuedRequest[] = []; const ratelimitedPaths = new Map(); let globallyRateLimited = false; let queueInProcess = false; +export interface QueuedRequest { + callback: () => Promise; + bucketID?: string | null; + url: string; +} + export interface RateLimitedPath { url: string; resetTimestamp: number; + bucketID: string | null; } async function processRateLimitedPaths() { const now = Date.now(); ratelimitedPaths.forEach((value, key) => { - if (value.resetTimestamp > now) return; + if (value.resetTimestamp < now) return; ratelimitedPaths.delete(key); if (key === "global") globallyRateLimited = false; }); @@ -28,8 +35,24 @@ async function processRateLimitedPaths() { async function processQueue() { if (queue.length && !globallyRateLimited) { - const callback = queue.shift(); - if (callback) await callback(); + const request = queue.shift(); + if (request?.bucketID) { + const rateLimitResetIn = checkRatelimits(request.bucketID); + const rateLimitedURLResetIn = checkRatelimits(request.url); + if (rateLimitResetIn) { + // This request is still rate limited readd to queue + queue.push(request); + } else if (rateLimitedURLResetIn) { + // This URL is rate limited readd to queue + queue.push(request); + } else { + // This request is not rate limite so it should be run + await request.callback(); + } + } else { + // This request has no bucket id so it should be processed + await request?.callback(); + } } if (queue.length) processQueue(); @@ -70,17 +93,19 @@ function createRequestBody(body: any, method: RequestMethod) { }; } -async function checkRatelimits(url: string) { +function checkRatelimits(url: string) { const ratelimited = ratelimitedPaths.get(url); const global = ratelimitedPaths.get("global"); const now = Date.now(); if (ratelimited && now < ratelimited.resetTimestamp) { - await delay(now - ratelimited.resetTimestamp); + return ratelimited.resetTimestamp - now; } if (global && now < global.resetTimestamp) { - await delay(now - global.resetTimestamp); + return global.resetTimestamp - now; } + + return false; } async function runMethod( @@ -88,13 +113,21 @@ async function runMethod( url: string, body?: unknown, retryCount = 0, + bucketID?: string | null, ) { return new Promise((resolve, reject) => { const callback = async () => { try { - await checkRatelimits(url); + const rateLimitResetIn = checkRatelimits(url); + if (rateLimitResetIn) { + return setTimeout( + () => runMethod(method, url, body, retryCount++, bucketID), + rateLimitResetIn, + ); + } + const response = await fetch(url, createRequestBody(body, method)); - processHeaders(url, response.headers); + const bucketIDFromHeaders = processHeaders(url, response.headers); handleStatusCode(response.status); // Sometimes Discord returns an empty 204 response that can't be made to JSON. @@ -107,7 +140,13 @@ async function runMethod( ) { if (retryCount > 10) throw new Error(Errors.RATE_LIMIT_RETRY_MAXED); await delay(json.retry_after); - return runMethod(method, url, body, retryCount++); + return runMethod( + method, + url, + body, + retryCount++, + bucketIDFromHeaders, + ); } return resolve(json); @@ -116,7 +155,11 @@ async function runMethod( } }; - queue.push(callback); + queue.push({ + callback, + bucketID, + url, + }); if (!queueInProcess) { queueInProcess = true; processQueue(); @@ -146,7 +189,6 @@ function handleStatusCode(status: number) { } function processHeaders(url: string, headers: Headers) { - // If a rate limit response is encountered this will become true and returned let ratelimited = false; // Get all useful headers @@ -154,7 +196,7 @@ function processHeaders(url: string, headers: Headers) { const resetTimestamp = headers.get("x-ratelimit-reset"); const retryAfter = headers.get("retry-after"); const global = headers.get("x-ratelimit-global"); - // const bucketID = headers.get("x-ratelimit-bucket"); + const bucketID = headers.get("x-ratelimit-bucket"); // If there is no remaining rate limit for this endpoint, we save it in cache if (remaining && remaining === "0") { @@ -162,8 +204,17 @@ function processHeaders(url: string, headers: Headers) { ratelimitedPaths.set(url, { url, - resetTimestamp: Number(resetTimestamp), + resetTimestamp: Number(resetTimestamp) * 1000, + bucketID, }); + + if (bucketID) { + ratelimitedPaths.set(bucketID, { + url, + resetTimestamp: Number(resetTimestamp) * 1000, + bucketID, + }); + } } // If there is no remaining global limit, we save it in cache @@ -174,9 +225,17 @@ function processHeaders(url: string, headers: Headers) { ratelimitedPaths.set("global", { url: "global", resetTimestamp: Date.now() + Number(retryAfter), + bucketID, }); + + if (bucketID) { + ratelimitedPaths.set(bucketID, { + url: "global", + resetTimestamp: Date.now() + Number(retryAfter), + bucketID, + }); + } } - // Returns a boolean to check if we need to request again once the rate limit resets - return ratelimited; + return ratelimited ? bucketID : undefined; } From 6cab42a667d4b2837f61c6a17279f36ddfbfd7ea Mon Sep 17 00:00:00 2001 From: Skillz Date: Tue, 19 May 2020 11:58:01 -0400 Subject: [PATCH 37/37] adding better and refactored permission handling --- debug.ts | 10 +++++ module/requestManager.ts | 3 +- module/shardingManager.ts | 2 +- structures/channel.ts | 18 ++++----- structures/guild.ts | 50 ++++++++++++------------- structures/member.ts | 77 +++++++++++++++++++++++++++++++++------ structures/message.ts | 13 +++---- types/errors.ts | 1 + utils/permissions.ts | 57 ++++++++++++++++++++++++----- 9 files changed, 165 insertions(+), 66 deletions(-) diff --git a/debug.ts b/debug.ts index a15587f23..5d9984a8b 100644 --- a/debug.ts +++ b/debug.ts @@ -1,3 +1,13 @@ +/* + * This File will never run when you use it. + * It is only meant for easy debugging/adding features to the library. + * It allows me to easily run up a bot using the library itself without having to commit code + * and then reloading cache from another bot folder to then test each micro change. + * Especially since a lot of Deno is still unstable and we have to be able to adjust on the fly this is helpful. + * Don't worry this will never run and you should never touch this file. + * Review the official boilerplates to see how to start a bot! + */ + import Client from "./module/client.ts"; import { configs } from "./configs.ts"; import { Intents } from "./types/options.ts"; diff --git a/module/requestManager.ts b/module/requestManager.ts index 36fdd07e7..bdad9546d 100644 --- a/module/requestManager.ts +++ b/module/requestManager.ts @@ -36,6 +36,7 @@ async function processRateLimitedPaths() { async function processQueue() { if (queue.length && !globallyRateLimited) { const request = queue.shift(); + if (request?.bucketID) { const rateLimitResetIn = checkRatelimits(request.bucketID); const rateLimitedURLResetIn = checkRatelimits(request.url); @@ -46,7 +47,7 @@ async function processQueue() { // This URL is rate limited readd to queue queue.push(request); } else { - // This request is not rate limite so it should be run + // This request is not rate limited so it should be run await request.callback(); } } else { diff --git a/module/shardingManager.ts b/module/shardingManager.ts index c805cd1f8..546cdc769 100644 --- a/module/shardingManager.ts +++ b/module/shardingManager.ts @@ -190,7 +190,7 @@ function handleDiscordPayload(data: DiscordPayload) { guild.memberCount = memberCount; const member = createMember( options, - options.guild_id, + guild.id, [...guild.roles.values()].map((role) => role.raw), guild.owner_id, ); diff --git a/structures/channel.ts b/structures/channel.ts index f4da7c327..a68ded54a 100644 --- a/structures/channel.ts +++ b/structures/channel.ts @@ -8,7 +8,7 @@ import { CreateInviteOptions, ChannelEditOptions, } from "../types/channel.ts"; -import { botID, updateChannelCache } from "../module/client.ts"; +import { updateChannelCache } from "../module/client.ts"; import { endpoints } from "../constants/discord.ts"; import { createMessage } from "./message.ts"; import { MessageCreateOptions } from "../types/message.ts"; @@ -43,14 +43,13 @@ export function createChannel(data: ChannelCreatePayload) { getMessage: async (id: string) => { if (data.guild_id) { if ( - !botHasPermission(data.guild_id, botID, [Permissions.VIEW_CHANNEL]) + !botHasPermission(data.guild_id, [Permissions.VIEW_CHANNEL]) ) { throw new Error(Errors.MISSING_VIEW_CHANNEL); } if ( !botHasPermission( data.guild_id, - botID, [Permissions.READ_MESSAGE_HISTORY], ) ) { @@ -72,14 +71,13 @@ export function createChannel(data: ChannelCreatePayload) { ) => { if (data.guild_id) { if ( - !botHasPermission(data.guild_id, botID, [Permissions.VIEW_CHANNEL]) + !botHasPermission(data.guild_id, [Permissions.VIEW_CHANNEL]) ) { throw new Error(Errors.MISSING_VIEW_CHANNEL); } if ( !botHasPermission( data.guild_id, - botID, [Permissions.READ_MESSAGE_HISTORY], ) ) { @@ -108,7 +106,7 @@ export function createChannel(data: ChannelCreatePayload) { if (data.guild_id) { if ( - !botHasPermission(data.guild_id, botID, [Permissions.SEND_MESSAGES]) + !botHasPermission(data.guild_id, [Permissions.SEND_MESSAGES]) ) { throw new Error(Errors.MISSING_SEND_MESSAGES); } @@ -116,7 +114,6 @@ export function createChannel(data: ChannelCreatePayload) { content.tts && !botHasPermission( data.guild_id, - botID, [Permissions.SEND_TTS_MESSAGES], ) ) { @@ -140,7 +137,7 @@ export function createChannel(data: ChannelCreatePayload) { deleteMessages: (ids: string[], reason?: string) => { if ( data.guild_id && - !botHasPermission(data.guild_id, botID, [Permissions.MANAGE_MESSAGES]) + !botHasPermission(data.guild_id, [Permissions.MANAGE_MESSAGES]) ) { throw new Error(Errors.MISSING_MANAGE_MESSAGES); } @@ -161,7 +158,7 @@ export function createChannel(data: ChannelCreatePayload) { getInvites: () => { if ( data.guild_id && - !botHasPermission(data.guild_id, botID, [Permissions.MANAGE_CHANNELS]) + !botHasPermission(data.guild_id, [Permissions.MANAGE_CHANNELS]) ) { throw new Error(Errors.MISSING_MANAGE_CHANNELS); } @@ -173,7 +170,6 @@ export function createChannel(data: ChannelCreatePayload) { data.guild_id && !botHasPermission( data.guild_id, - botID, [Permissions.CREATE_INSTANT_INVITE], ) ) { @@ -185,7 +181,7 @@ export function createChannel(data: ChannelCreatePayload) { getWebhooks: () => { if ( data.guild_id && - !botHasPermission(data.guild_id, botID, [Permissions.MANAGE_WEBHOOKS]) + !botHasPermission(data.guild_id, [Permissions.MANAGE_WEBHOOKS]) ) { throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); } diff --git a/structures/guild.ts b/structures/guild.ts index f45e67218..ad60fc61c 100644 --- a/structures/guild.ts +++ b/structures/guild.ts @@ -1,4 +1,4 @@ -import { botID, identifyPayload } from "../module/client.ts"; +import { identifyPayload } from "../module/client.ts"; import { endpoints } from "../constants/discord.ts"; import { formatImageURL } from "../utils/cdn.ts"; import { @@ -81,7 +81,7 @@ export const createGuild = (data: CreateGuildPayload) => { : undefined, /** Create a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */ createChannel: async (name: string, options: CreateChannelOptions) => { - if (!botHasPermission(data.id, botID, [Permissions.MANAGE_CHANNELS])) { + if (!botHasPermission(data.id, [Permissions.MANAGE_CHANNELS])) { throw new Error(Errors.MISSING_MANAGE_CHANNELS); } const result = @@ -133,7 +133,7 @@ export const createGuild = (data: CreateGuildPayload) => { options: CreateEmojisOptions, ) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_EMOJIS]) + !botHasPermission(data.id, [Permissions.MANAGE_EMOJIS]) ) { throw new Error(Errors.MISSING_MANAGE_EMOJIS); } @@ -146,7 +146,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Modify the given emoji. Requires the MANAGE_EMOJIS permission. */ editEmoji: (id: string, options: EditEmojisOptions) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_EMOJIS]) + !botHasPermission(data.id, [Permissions.MANAGE_EMOJIS]) ) { throw new Error(Errors.MISSING_MANAGE_EMOJIS); } @@ -158,7 +158,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Delete the given emoji. Requires the MANAGE_EMOJIS permission. Returns 204 No Content on success. */ deleteEmoji: (id: string, reason?: string) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_EMOJIS]) + !botHasPermission(data.id, [Permissions.MANAGE_EMOJIS]) ) { throw new Error(Errors.MISSING_MANAGE_EMOJIS); } @@ -170,7 +170,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Create a new role for the guild. Requires the MANAGE_ROLES permission. */ createRole: async (options: CreateRoleOptions, reason?: string) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_ROLES]) + !botHasPermission(data.id, [Permissions.MANAGE_ROLES]) ) { throw new Error(Errors.MISSING_MANAGE_ROLES); } @@ -191,7 +191,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Edit a guild role. Requires the MANAGE_ROLES permission. */ editRole: (id: string, options: CreateRoleOptions) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_ROLES]) + !botHasPermission(data.id, [Permissions.MANAGE_ROLES]) ) { throw new Error(Errors.MISSING_MANAGE_ROLES); } @@ -200,7 +200,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Delete a guild role. Requires the MANAGE_ROLES permission. */ deleteRole: (id: string) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_ROLES]) + !botHasPermission(data.id, [Permissions.MANAGE_ROLES]) ) { throw new Error(Errors.MISSING_MANAGE_ROLES); } @@ -212,7 +212,7 @@ export const createGuild = (data: CreateGuildPayload) => { */ getRoles: () => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_ROLES]) + !botHasPermission(data.id, [Permissions.MANAGE_ROLES]) ) { throw new Error(Errors.MISSING_MANAGE_ROLES); } @@ -221,7 +221,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Modify the positions of a set of role objects for the guild. Requires the MANAGE_ROLES permission. */ swapRoles: (rolePositons: PositionSwap) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_ROLES]) + !botHasPermission(data.id, [Permissions.MANAGE_ROLES]) ) { throw new Error(Errors.MISSING_MANAGE_ROLES); } @@ -231,7 +231,7 @@ export const createGuild = (data: CreateGuildPayload) => { getPruneCount: async (days: number) => { if (days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); if ( - !botHasPermission(data.id, botID, [Permissions.KICK_MEMBERS]) + !botHasPermission(data.id, [Permissions.KICK_MEMBERS]) ) { throw new Error(Errors.MISSING_KICK_MEMBERS); } @@ -245,7 +245,7 @@ export const createGuild = (data: CreateGuildPayload) => { pruneMembers: (days: number) => { if (days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); if ( - !botHasPermission(data.id, botID, [Permissions.KICK_MEMBERS]) + !botHasPermission(data.id, [Permissions.KICK_MEMBERS]) ) { throw new Error(Errors.MISSING_KICK_MEMBERS); } @@ -262,7 +262,7 @@ export const createGuild = (data: CreateGuildPayload) => { }, /** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */ getAuditLogs: (options: GetAuditLogsOptions) => { - if (!botHasPermission(data.id, botID, [Permissions.VIEW_AUDIT_LOG])) { + if (!botHasPermission(data.id, [Permissions.VIEW_AUDIT_LOG])) { throw new Error(Errors.MISSING_VIEW_AUDIT_LOG); } @@ -276,7 +276,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Returns the guild embed object. Requires the MANAGE_GUILD permission. */ getEmbed: () => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_GUILD]) + !botHasPermission(data.id, [Permissions.MANAGE_GUILD]) ) { throw new Error(Errors.MISSING_MANAGE_GUILD); } @@ -285,7 +285,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Modify a guild embed object for the guild. Requires the MANAGE_GUILD permission. */ editEmbed: (enabled: boolean, channel_id?: string | null) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_GUILD]) + !botHasPermission(data.id, [Permissions.MANAGE_GUILD]) ) { throw new Error(Errors.MISSING_MANAGE_GUILD); } @@ -301,7 +301,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Returns a list of integrations for the guild. Requires the MANAGE_GUILD permission. */ getIntegrations: () => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_GUILD]) + !botHasPermission(data.id, [Permissions.MANAGE_GUILD]) ) { throw new Error(Errors.MISSING_MANAGE_GUILD); } @@ -310,7 +310,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Modify the behavior and settings of an integration object for the guild. Requires the MANAGE_GUILD permission. */ editIntegration: (id: string, options: EditIntegrationOptions) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_GUILD]) + !botHasPermission(data.id, [Permissions.MANAGE_GUILD]) ) { throw new Error(Errors.MISSING_MANAGE_GUILD); } @@ -322,7 +322,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Delete the attached integration object for the guild with this id. Requires MANAGE_GUILD permission. */ deleteIntegration: (id: string) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_GUILD]) + !botHasPermission(data.id, [Permissions.MANAGE_GUILD]) ) { throw new Error(Errors.MISSING_MANAGE_GUILD); } @@ -331,7 +331,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Sync an integration. Requires teh MANAGE_GUILD permission. */ syncIntegration: (id: string) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_GUILD]) + !botHasPermission(data.id, [Permissions.MANAGE_GUILD]) ) { throw new Error(Errors.MISSING_MANAGE_GUILD); } @@ -340,7 +340,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */ getBans: () => { if ( - !botHasPermission(data.id, botID, [Permissions.BAN_MEMBERS]) + !botHasPermission(data.id, [Permissions.BAN_MEMBERS]) ) { throw new Error(Errors.MISSING_BAN_MEMBERS); } @@ -349,7 +349,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Ban a user from the guild and optionally delete previous messages sent by the user. Requires teh BAN_MEMBERS permission. */ ban: (id: string, options: BanOptions) => { if ( - !botHasPermission(data.id, botID, [Permissions.BAN_MEMBERS]) + !botHasPermission(data.id, [Permissions.BAN_MEMBERS]) ) { throw new Error(Errors.MISSING_BAN_MEMBERS); } @@ -358,7 +358,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Remove the ban for a user. REquires BAN_MEMBERS permission */ unban: (id: string) => { if ( - !botHasPermission(data.id, botID, [Permissions.BAN_MEMBERS]) + !botHasPermission(data.id, [Permissions.BAN_MEMBERS]) ) { throw new Error(Errors.MISSING_BAN_MEMBERS); } @@ -400,7 +400,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Modify a guilds settings. Requires the MANAGE_GUILD permission. */ edit: (options: GuildEditOptions) => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_GUILD]) + !botHasPermission(data.id, [Permissions.MANAGE_GUILD]) ) { throw new Error(Errors.MISSING_MANAGE_GUILD); } @@ -409,7 +409,7 @@ export const createGuild = (data: CreateGuildPayload) => { /** Get all the invites for this guild. Requires MANAGE_GUILD permission */ getInvites: () => { if ( - !botHasPermission(data.id, botID, [Permissions.MANAGE_GUILD]) + !botHasPermission(data.id, [Permissions.MANAGE_GUILD]) ) { throw new Error(Errors.MISSING_MANAGE_GUILD); } @@ -425,7 +425,7 @@ export const createGuild = (data: CreateGuildPayload) => { }, /** Returns a list of guild webhooks objects. Requires the MANAGE_WEBHOOKs permission. */ getWebhooks: () => { - if (!botHasPermission(data.id, botID, [Permissions.MANAGE_WEBHOOKS])) { + if (!botHasPermission(data.id, [Permissions.MANAGE_WEBHOOKS])) { throw new Error(Errors.MISSING_MANAGE_WEBHOOKS); } diff --git a/structures/member.ts b/structures/member.ts index 7b17ea8cc..4f5c2bd16 100644 --- a/structures/member.ts +++ b/structures/member.ts @@ -1,13 +1,18 @@ -import { botID } from "../module/client.ts"; import { endpoints } from "../constants/discord.ts"; import { formatImageURL } from "../utils/cdn.ts"; import { MemberCreatePayload, EditMemberOptions } from "../types/member.ts"; import { ImageSize, ImageFormats } from "../types/cdn.ts"; import { Permission, Permissions } from "../types/permission.ts"; import { RoleData } from "../types/role.ts"; -import { memberHasPermission, botHasPermission } from "../utils/permissions.ts"; +import { + memberHasPermission, + botHasPermission, + highestRole, + higherRolePosition, +} from "../utils/permissions.ts"; import { Errors } from "../types/errors.ts"; import { RequestManager } from "../module/requestManager.ts"; +import { botID } from "../module/client.ts"; export const createMember = ( data: MemberCreatePayload, @@ -38,12 +43,18 @@ export const createMember = ( : endpoints.USER_DEFAULT_AVATAR(Number(data.user.discriminator) % 5), /** Add a role to the member */ addRole: (roleID: string, reason?: string) => { - // TODO: check if the bots highest role is above this one + const botsHighestRole = highestRole(guildID, botID); if ( - !botHasPermission(guildID, botID, [Permissions.MANAGE_ROLES]) + botsHighestRole && + !higherRolePosition(guildID, botsHighestRole.id, roleID) ) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + } + + if (!botHasPermission(guildID, [Permissions.MANAGE_ROLES])) { throw new Error(Errors.MISSING_MANAGE_ROLES); } + return RequestManager.put( endpoints.GUILD_MEMBER_ROLE(guildID, data.user.id, roleID), { reason }, @@ -51,10 +62,15 @@ export const createMember = ( }, /** Remove a role from the member */ remove_role: (roleID: string, reason?: string) => { - // TODO: check if the bots highest role is above this role + const botsHighestRole = highestRole(guildID, botID); if ( - !botHasPermission(guildID, botID, [Permissions.MANAGE_ROLES]) + botsHighestRole && + !higherRolePosition(guildID, botsHighestRole.id, roleID) ) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + } + + if (!botHasPermission(guildID, [Permissions.MANAGE_ROLES])) { throw new Error(Errors.MISSING_MANAGE_ROLES); } return RequestManager.delete( @@ -64,10 +80,16 @@ export const createMember = ( }, /** Kick a member from the server */ kick: (reason?: string) => { - // TODO: Check if the bot is above the user so it is capable of kicking + const botsHighestRole = highestRole(guildID, botID); + const membersHighestRole = highestRole(guildID, data.user.id); if ( - !botHasPermission(guildID, botID, [Permissions.KICK_MEMBERS]) + botsHighestRole && membersHighestRole && + botsHighestRole.position <= membersHighestRole.position ) { + throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW); + } + + if (!botHasPermission(guildID, [Permissions.KICK_MEMBERS])) { throw new Error(Errors.MISSING_KICK_MEMBERS); } return RequestManager.delete( @@ -81,14 +103,14 @@ export const createMember = ( if (options.nick.length > 32) { throw new Error(Errors.NICKNAMES_MAX_LENGTH); } - if (!botHasPermission(guildID, botID, [Permissions.MANAGE_NICKNAMES])) { + if (!botHasPermission(guildID, [Permissions.MANAGE_NICKNAMES])) { throw new Error(Errors.MISSING_MANAGE_NICKNAMES); } } if ( options.roles && - !botHasPermission(guildID, botID, [Permissions.MANAGE_ROLES]) + !botHasPermission(guildID, [Permissions.MANAGE_ROLES]) ) { throw new Error(Errors.MISSING_MANAGE_ROLES); } @@ -96,7 +118,7 @@ export const createMember = ( if (options.mute) { // TODO: This should check if the member is in a voice channel if ( - !botHasPermission(guildID, botID, [Permissions.MUTE_MEMBERS]) + !botHasPermission(guildID, [Permissions.MUTE_MEMBERS]) ) { throw new Error(Errors.MISSING_MUTE_MEMBERS); } @@ -104,7 +126,7 @@ export const createMember = ( if ( options.deaf && - !botHasPermission(guildID, botID, [Permissions.DEAFEN_MEMBERS]) + !botHasPermission(guildID, [Permissions.DEAFEN_MEMBERS]) ) { throw new Error(Errors.MISSING_DEAFEN_MEMBERS); } @@ -127,3 +149,34 @@ export const createMember = ( ); }, }); + + + +| Name | Language | +| ------------------------------------------------------------ | ---------- | +| [discljord](https://github.com/igjoshua/discljord) | Clojure | +| [aegis.cpp](https://github.com/zeroxs/aegis.cpp) | C++ | +| [discordcr](https://github.com/discordcr/discordcr) | Crystal | +| [Discord.Net](https://github.com/RogueException/Discord.Net) | C# | +| [DSharpPlus](https://github.com/DSharpPlus/DSharpPlus) | C# | +| [dscord](https://github.com/b1naryth1ef/dscord) | D | +| [DiscordGo](https://github.com/bwmarrin/discordgo) | Go | +| [DisGord](https://github.com/andersfylling/disgord) | Go | +| [catnip](https://github.com/mewna/catnip) | Java | +| [Discord4J](https://discord4j.com/) | Java | +| [Javacord](https://github.com/Javacord/Javacord) | Java | +| [JDA](https://github.com/DV8FromTheWorld/JDA) | Java | +| [discord.js](https://github.com/discordjs/discord.js) | JavaScript | +| [Eris](https://github.com/abalabahaha/eris) | JavaScript | +| [Discord.jl](https://github.com/Xh4H/Discord.jl) | Julia | +| [Discordia](https://github.com/SinisterRectus/Discordia) | Lua | +| [discordnim](https://github.com/Krognol/discordnim) | Nim | +| [RestCord](https://www.restcord.com/) | PHP | +| [discord.py](https://github.com/Rapptz/discord.py) | Python | +| [disco](https://github.com/b1naryth1ef/disco) | Python | +| [discordrb](https://github.com/discordrb/discordrb) | Ruby | +| [discord-rs](https://github.com/SpaceManiac/discord-rs) | Rust | +| [Serenity](https://github.com/serenity-rs/serenity) | Rust | +| [AckCord](https://github.com/Katrix/AckCord) | Scala | +| [Sword](https://github.com/Azoy/Sword) | Swift | +| [Discordeno](https://github.com/Skillz4Killz/Discordeno) | Typescript(Deno) | diff --git a/structures/message.ts b/structures/message.ts index 1198ed0c0..e9f8e8cc8 100644 --- a/structures/message.ts +++ b/structures/message.ts @@ -26,7 +26,7 @@ export function createMessage(data: MessageCreateOptions) { delete: (reason?: string) => { if ( data.guild_id && - !botHasPermission(data.guild_id, botID, [Permissions.MANAGE_MESSAGES]) + !botHasPermission(data.guild_id, [Permissions.MANAGE_MESSAGES]) ) { throw new Error(Errors.MISSING_MANAGE_MESSAGES); } @@ -42,7 +42,7 @@ export function createMessage(data: MessageCreateOptions) { pin: () => { if ( data.guild_id && - !botHasPermission(data.guild_id, botID, [Permissions.MANAGE_MESSAGES]) + !botHasPermission(data.guild_id, [Permissions.MANAGE_MESSAGES]) ) { throw new Error(Errors.MISSING_MANAGE_MESSAGES); } @@ -51,7 +51,7 @@ export function createMessage(data: MessageCreateOptions) { unpin: () => { if ( data.guild_id && - !botHasPermission(data.guild_id, botID, [Permissions.MANAGE_MESSAGES]) + !botHasPermission(data.guild_id, [Permissions.MANAGE_MESSAGES]) ) { throw new Error(Errors.MISSING_MANAGE_MESSAGES); } @@ -83,7 +83,7 @@ export function createMessage(data: MessageCreateOptions) { removeAllReactions: () => { if ( data.guild_id && - !botHasPermission(data.guild_id, botID, [Permissions.MANAGE_MESSAGES]) + !botHasPermission(data.guild_id, [Permissions.MANAGE_MESSAGES]) ) { throw new Error(Errors.MISSING_MANAGE_MESSAGES); } @@ -95,7 +95,7 @@ export function createMessage(data: MessageCreateOptions) { removeReactionEmoji: (reaction: string) => { if ( data.guild_id && - !botHasPermission(data.guild_id, botID, [Permissions.MANAGE_MESSAGES]) + !botHasPermission(data.guild_id, [Permissions.MANAGE_MESSAGES]) ) { throw new Error(Errors.MISSING_MANAGE_MESSAGES); } @@ -122,7 +122,7 @@ export function createMessage(data: MessageCreateOptions) { if (data.guild_id) { if ( - !botHasPermission(data.guild_id, botID, [Permissions.SEND_MESSAGES]) + !botHasPermission(data.guild_id, [Permissions.SEND_MESSAGES]) ) { throw new Error(Errors.MISSING_SEND_MESSAGES); } @@ -131,7 +131,6 @@ export function createMessage(data: MessageCreateOptions) { content.tts && !botHasPermission( data.guild_id, - botID, [Permissions.SEND_TTS_MESSAGES], ) ) { diff --git a/types/errors.ts b/types/errors.ts index c91cc1fad..c04a71baf 100644 --- a/types/errors.ts +++ b/types/errors.ts @@ -26,4 +26,5 @@ export enum Errors { REQUEST_CLIENT_ERROR = "REQUEST_CLIENT_ERROR", REQUEST_SERVER_ERROR = "REQUEST_SERVER_ERROR", REQUEST_UNKNOWN_ERROR = "REQUEST_UNKNOWN_ERROR", + BOTS_HIGHEST_ROLE_TOO_LOW = "BOTS_HIGHEST_ROLE_TOO_LOW", } diff --git a/utils/permissions.ts b/utils/permissions.ts index c023f1c29..c5a7f5e06 100644 --- a/utils/permissions.ts +++ b/utils/permissions.ts @@ -1,14 +1,16 @@ import { Permission, Permissions } from "../types/permission.ts"; import { RoleData } from "../types/role.ts"; import { cache } from "./cache.ts"; +import { botID } from "../module/client.ts"; +import { Role } from "../structures/role.ts"; -export const memberHasPermission = ( +export function memberHasPermission( member_id: string, owner_id: string, role_data: RoleData[], member_role_ids: string[], permissions: Permission[], -) => { +) { if (member_id === owner_id) return true; const permissionBits = role_data @@ -24,13 +26,12 @@ export const memberHasPermission = ( return permissions.every((permission) => permissionBits & Permissions[permission] ); -}; +} -export const botHasPermission = ( +export function botHasPermission( guild_id: string, - botID: string, permissions: Permissions[], -) => { +) { const guild = cache.guilds.get(guild_id); if (!guild) return false; @@ -49,10 +50,48 @@ export const botHasPermission = ( if (permissionBits & Permissions.ADMINISTRATOR) return true; return permissions.every((permission) => permissionBits & permission); -}; +} -export const calculatePermissions = (permission_bits: number) => { +export function calculatePermissions(permission_bits: number) { return Object.keys(Permissions).filter((perm) => { return permission_bits & Permissions[perm as Permission]; }); -}; +} + +export function highestRole(guildID: string, memberID: string) { + const guild = cache.guilds.get(guildID); + if (!guild) return; + + const member = guild?.members.get(memberID); + if (!member) return; + + let memberHighestRole: Role | undefined; + + for (const roleID of member.roles) { + const role = guild.roles.get(roleID); + if (!role) continue; + + if ( + !memberHighestRole || memberHighestRole.position < role.position + ) { + memberHighestRole = role; + } + } + + return memberHighestRole || (guild.roles.get(guild.id) as Role); +} + +export function higherRolePosition( + guildID: string, + roleID: string, + otherRoleID: string, +) { + const guild = cache.guilds.get(guildID); + if (!guild) return; + + const role = guild.roles.get(roleID); + const otherRole = guild.roles.get(otherRoleID); + if (!role || !otherRole) return; + + return role.position > otherRole.position; +}