diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml deleted file mode 100644 index d65aeb3b4..000000000 --- a/.github/workflows/greetings.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Greetings - -on: [pull_request, issues] - -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" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..159b4f247 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,17 @@ +name: Test +on: + push: + branches: + - master +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: denolib/setup-deno@master + - name: Cache dependencies + run: deno cache mod.ts + - name: Run test script + run: deno test -A + env: + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d0ae29732..f5003566b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,8 @@ "deno.import_intellisense_origins": { "https://deno.land": true }, + "editor.codeActionsOnSave": { + "source.organizeImports": true + }, "editor.defaultFormatter": "denoland.vscode-deno" } diff --git a/README.md b/README.md index 4d12951fa..bd5888489 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,19 @@ > Discord API library wrapper in Deno -[![Discord](https://img.shields.io/discord/223909216866402304?color=7289da&logo=discord&logoColor=white)](https://discord.gg/J4NqJ72) +[![Discord](https://img.shields.io/discord/223909216866402304?color=7289da&logo=discord&logoColor=dark)](https://discord.gg/J4NqJ72) ![Testing/Linting](https://github.com/Skillz4Killz/Discordeno/workflows/Testing/Linting/badge.svg) -[![nest.land](https://nest.land/badge-large.svg)](https://nest.land/package/Discordeno) +[![nest badge](https://nest.land/badge.svg)](https://nest.land/package/Discordeno) -[Website](https://discordeno.netlify.app) +[WIP] ![Test](https://github.com/Skillz4Killz/Discordeno/workflows/Test/badge.svg) -## Beginner Developers +## Why Discordeno? -Don't worry a lot of developers start out coding their first projects as a Discord bot(I did 😉) and it is not so easy. With Discordeno, I tried to build it in a way that solved all the headaches I had when first starting out coding bots. If you are a beginner developer, please use a boilerplate: The official one is at: [GitHub](https://github.com/Skillz4Killz/Discordeno-bot-template) but there will be more listed on the website. It is a beautiful website indeed! Check it out! +### Beginner Developers + +Don't worry a lot of developers start out coding their first projects as a Discord bot (I did 😉) and it is not so easy to do so. Discordeno is built considering all the issues wit pre-existing libraries, such as discord.js, and issues that I had when I first started out coding bots. + +If you are a beginner developer, please use this official boilerplate: [GitHub](https://github.com/Skillz4Killz/Discordeno-bot-template) but there will be more listed on the website. It is a beautiful website indeed! Check it out! **Modular commands, arguments, events, inhibitors, monitors, tasks.** @@ -41,15 +45,10 @@ Don't worry a lot of developers start out coding their first projects as a Disco - Uses i18next, one of the best localization tools available. - Supports nested folders to keep cleaner translation files -**Hot Reloadable** +- **Hot Reloadable**: Easily update your code without having to restart the bot everytime. +- **Step By Step Guide**: There is a step by step walkthrough to learn how to create Discord bots with Discordeno on our website! -- Easily update your code without having to restart the bot everytime. - -**Step By Step Guide** - -- There is a step by step walkthrough to learn how to create Discord bots with Discordeno on our website! - -## Advanced Developers +### Advanced Developers The instructions below are meant for advanced developers! @@ -64,7 +63,7 @@ StartBot({ intents: [Intents.GUILD_MESSAGES, Intents.GUILDS], eventHandlers: { ready: () => { - console.log(`Logged!`); + console.log('Successfully connected to gateway'); }, messageCreate: (message) => { if (message.content === "!ping") { @@ -77,8 +76,13 @@ StartBot({ Alternatively, you can use boilerplate template repositories that were created by wonderful developers. Review the list on the website, and add any of yours if you make your own. -![image](https://i.imgur.com/z1BfUnt.png) +## Documentation -#### Dark Mode +- [API Documentation](https://doc.deno.land/https/deno.land/x/discordeno/mod.ts) +- [Guide](https://discordeno.netlify.com) +- [Support server](https://discord.gg/J4NqJ72) +- [Contributing Guide](https://github.com/Skillz4Killz/Discordeno/blob/master/.github/CONTRIBUTING.md) -![image](https://i.imgur.com/Vr2Bebr.png) +## License + +MIT © Skillz4Killz diff --git a/deps.ts b/deps.ts index 776b73054..ddc71fbf2 100644 --- a/deps.ts +++ b/deps.ts @@ -7,4 +7,9 @@ export { isWebSocketPongEvent, } from "https://deno.land/std@0.67.0/ws/mod.ts"; export type { WebSocket } from "https://deno.land/std@0.67.0/ws/mod.ts"; +export { + assert, + assertArrayIncludes, + assertEquals, +} from "https://deno.land/std@0.75.0/testing/asserts.ts"; export { decompress_with as inflate } from "https://unpkg.com/@evan/wasm@0.0.11/target/zlib/deno.js"; diff --git a/docs/content/advanced/dynamiccommands.md b/docs/content/advanced/dynamiccommands.md index 313ee0719..a68da76e1 100644 --- a/docs/content/advanced/dynamiccommands.md +++ b/docs/content/advanced/dynamiccommands.md @@ -54,43 +54,44 @@ const nekosEndpoints = [ { name: "gecg", path: "/img/gecg", nsfw: false }, { name: "avatar", path: "/img/avatar", nsfw: false }, { name: "waifu", path: "/img/waifu", nsfw: false }, - { name: "randomHentaiGif", path: "/img/Random_hentai_gif", nsfw: true }, - { name: "pussy", path: "/img/pussy", nsfw: true }, - { name: "nekoGif", path: "/img/nsfw_neko_gif", nsfw: true }, - { name: "neko", path: "/img/lewd", nsfw: true }, - { name: "lesbian", path: "/img/les", nsfw: true }, - { name: "kuni", path: "/img/kuni", nsfw: true }, - { name: "cumsluts", path: "/img/cum", nsfw: true }, - { name: "classic", path: "/img/classic", nsfw: true }, - { name: "boobs", path: "/img/boobs", nsfw: true }, - { name: "bJ", path: "/img/bj", nsfw: true }, - { name: "anal", path: "/img/anal", nsfw: true }, - { name: "avatar", path: "/img/nsfw_avatar", nsfw: true }, - { name: "yuri", path: "/img/yuri", nsfw: true }, - { name: "trap", path: "/img/trap", nsfw: true }, - { name: "tits", path: "/img/tits", nsfw: true }, - { name: "girlSoloGif", path: "/img/solog", nsfw: true }, - { name: "girlSolo", path: "/img/solo", nsfw: true }, - { name: "pussyWankGif", path: "/img/pwankg", nsfw: true }, - { name: "pussyArt", path: "/img/pussy_jpg", nsfw: true }, - { name: "kemonomimi", path: "/img/lewdkemo", nsfw: true }, - { name: "kitsune", path: "/img/lewdk", nsfw: true }, - { name: "keta", path: "/img/keta", nsfw: true }, - { name: "holo", path: "/img/hololewd", nsfw: true }, - { name: "holoEro", path: "/img/holoero", nsfw: true }, - { name: "hentai", path: "/img/hentai", nsfw: true }, - { name: "futanari", path: "/img/futanari", nsfw: true }, - { name: "femdom", path: "/img/femdom", nsfw: true }, - { name: "feetGif", path: "/img/feetg", nsfw: true }, - { name: "eroFeet", path: "/img/erofeet", nsfw: true }, - { name: "feet", path: "/img/feet", nsfw: true }, - { name: "ero", path: "/img/ero", nsfw: true }, - { name: "eroKitsune", path: "/img/erok", nsfw: true }, - { name: "eroKemonomimi", path: "/img/erokemo", nsfw: true }, - { name: "eroNeko", path: "/img/eron", nsfw: true }, - { name: "eroYuri", path: "/img/eroyuri", nsfw: true }, - { name: "cumArts", path: "/img/cum_jpg", nsfw: true }, - { name: "blowJob", path: "/img/blowjob", nsfw: true }, + // The follow name and paths have been hidden for this guide as they are NSFW. + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, + { name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true }, ]; nekosEndpoints.forEach((endpoint) => { diff --git a/docs/content/djs.md b/docs/content/djs.md index 593d00b55..afc0465a9 100644 --- a/docs/content/djs.md +++ b/docs/content/djs.md @@ -110,9 +110,9 @@ import Client, { import { configs } from "./configs.ts"; import { Intents } from "https://x.nest.land/Discordeno@9.0.1/src/types/options.ts"; import { eventHandlers } from "./src/events/eventHandlers.ts"; -import type { Message } from "https://x.nest.land/Discordeno@9.0.1/src/structures/message.ts"; -import type { Command } from "./src/types/commands.ts"; -import type { Guild } from "https://x.nest.land/Discordeno@9.0.1/src/structures/guild.ts"; +import { Message } from "https://x.nest.land/Discordeno@9.0.1/src/structures/message.ts"; +import { Command } from "./src/types/commands.ts"; +import { Guild } from "https://x.nest.land/Discordeno@9.0.1/src/structures/guild.ts"; export const botCache = { commands: new Map(), @@ -346,7 +346,7 @@ module.exports = class kickCommand extends Command { Discordeno Version ```ts import { sendMessage } from "https://x.nest.land/Discordeno@9.0.1/src/handlers/channel.ts"; -import type { Member } from "https://x.nest.land/Discordeno@9.0.1/src/structures/member.ts"; +import { Member } from "https://x.nest.land/Discordeno@9.0.1/src/structures/member.ts"; import { kick } from "https://x.nest.land/Discordeno@9.0.1/src/handlers/member.ts"; import { deleteMessage } from "https://x.nest.land/Discordeno@9.0.1/src/handlers/message.ts"; import { botCache } from "../../mod.ts"; diff --git a/docs/content/gettingstarted.md b/docs/content/gettingstarted.md index 1c2c98e47..6ee051f24 100644 --- a/docs/content/gettingstarted.md +++ b/docs/content/gettingstarted.md @@ -75,7 +75,8 @@ Alternatively, you can use boilerplate template repositories that were created b | 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. | | Discordeno Helper | Suyashtnt | [Github](https://github.com/Suyashtnt/discordeno-helper-template/) | A reimplementation of DenoBot using the [discordeno-helper](https://github.com/Suyashtnt/discordeno-helper) framework -Open Sourced Bots: +**Open Sourced Bots:** + | Bot Name | Developer | Links | | ----------------- | ---------- | ---------------------------------------------------------- | | discordeno-mattis | Mattis6666 | [Github](https://github.com/Mattis6666/discordeno-mattis/) | diff --git a/docs/content/introduction.md b/docs/content/introduction.md index 29c6b408c..effb978f7 100644 --- a/docs/content/introduction.md +++ b/docs/content/introduction.md @@ -19,6 +19,12 @@ Discordeno is a Third Party Deno Library for interacting with the Discord API. - Latest and Greatest JavaScript - Actively Maintained! +### User Reviews + +If you wish to leave a review for other users, please send a PR adding your review to this section! + +Using the Discord API with types but such a simple language like TypeScript is so easy now. Discordeno is A W E S O M E! -[LukasDoesDev](https://github.com/LukasDoesDev) + ## Read me first... Discordeno is cool right? You could make the next big bot! Who knows, but before we get right into developing our Bot. We want to get started with learning the basics... diff --git a/egg.yml b/egg.yml index aaacd0120..79b0633d1 100644 --- a/egg.yml +++ b/egg.yml @@ -2,18 +2,16 @@ name: Discordeno description: >- Discord Deno TypeScript API library wrapper(Officially vetted library by Discord Team) https://discordeno.netlify.app -version: 9.0.1 +version: 9.0.15 stable: true -entry: /mod.ts +entry: mod.ts repository: 'https://github.com/Skillz4Killz/Discordeno' files: - ./src/**/* - LICENSE - - mod.ts - README.md - tsconfig.json - ./deps.ts - - ./mod.ts - - ./mod.ts + - mod.ts checkAll: false unlisted: false diff --git a/src/controllers/bans.ts b/src/controllers/bans.ts index dbd4cf10c..fca5c7f6f 100644 --- a/src/controllers/bans.ts +++ b/src/controllers/bans.ts @@ -1,6 +1,6 @@ import { eventHandlers } from "../module/client.ts"; -import type { DiscordPayload } from "../types/discord.ts"; -import type { GuildBanPayload } from "../types/guild.ts"; +import { DiscordPayload } from "../types/discord.ts"; +import { GuildBanPayload } from "../types/guild.ts"; import { cacheHandlers } from "./cache.ts"; export async function handleInternalGuildBanAdd(data: DiscordPayload) { diff --git a/src/controllers/cache.ts b/src/controllers/cache.ts index 676d45534..55829c14a 100644 --- a/src/controllers/cache.ts +++ b/src/controllers/cache.ts @@ -1,9 +1,9 @@ -import type { Channel } from "../structures/channel.ts"; -import type { Guild } from "../structures/guild.ts"; -import type { Message } from "../structures/message.ts"; -import type { PresenceUpdatePayload } from "../types/discord.ts"; +import { Channel } from "../structures/channel.ts"; +import { Guild } from "../structures/guild.ts"; +import { Message } from "../structures/message.ts"; +import { PresenceUpdatePayload } from "../types/discord.ts"; import { cache } from "../utils/cache.ts"; -import type { Collection } from "../utils/collection.ts"; +import { Collection } from "../utils/collection.ts"; export type TableName = | "guilds" diff --git a/src/controllers/channels.ts b/src/controllers/channels.ts index 36d78c576..1812e6c20 100644 --- a/src/controllers/channels.ts +++ b/src/controllers/channels.ts @@ -1,8 +1,7 @@ import { eventHandlers } from "../module/client.ts"; import { structures } from "../structures/mod.ts"; -import type { ChannelCreatePayload } from "../types/channel.ts"; -import { ChannelTypes } from "../types/channel.ts"; -import type { DiscordPayload } from "../types/discord.ts"; +import { ChannelCreatePayload, ChannelTypes } from "../types/channel.ts"; +import { DiscordPayload } from "../types/discord.ts"; import { cacheHandlers } from "./cache.ts"; export async function handleInternalChannelCreate(data: DiscordPayload) { diff --git a/src/controllers/guilds.ts b/src/controllers/guilds.ts index ef4d4c271..61a2777c6 100644 --- a/src/controllers/guilds.ts +++ b/src/controllers/guilds.ts @@ -1,13 +1,13 @@ import { eventHandlers } from "../module/client.ts"; import { structures } from "../structures/mod.ts"; -import type { DiscordPayload } from "../types/discord.ts"; -import type { +import { DiscordPayload } from "../types/discord.ts"; +import { CreateGuildPayload, GuildDeletePayload, GuildEmojisUpdatePayload, UpdateGuildPayload, } from "../types/guild.ts"; -import type { GuildUpdateChange } from "../types/options.ts"; +import { GuildUpdateChange } from "../types/options.ts"; import { cache } from "../utils/cache.ts"; import { cacheHandlers } from "./cache.ts"; diff --git a/src/controllers/members.ts b/src/controllers/members.ts index 6d413f90f..f1101525b 100644 --- a/src/controllers/members.ts +++ b/src/controllers/members.ts @@ -1,7 +1,7 @@ import { eventHandlers } from "../module/client.ts"; import { structures } from "../structures/mod.ts"; -import type { DiscordPayload } from "../types/discord.ts"; -import type { +import { DiscordPayload } from "../types/discord.ts"; +import { GuildBanPayload, GuildMemberAddPayload, GuildMemberChunkPayload, @@ -41,11 +41,6 @@ export async function handleInternalGuildMemberRemove(data: DiscordPayload) { member || payload.user, ); - eventHandlers.guildMemberRemove?.( - guild, - member || payload.user, - ); - guild.members.delete(payload.user.id); } diff --git a/src/controllers/messages.ts b/src/controllers/messages.ts index fe45ed022..3f4298858 100644 --- a/src/controllers/messages.ts +++ b/src/controllers/messages.ts @@ -1,7 +1,7 @@ import { eventHandlers } from "../module/client.ts"; import { structures } from "../structures/mod.ts"; -import type { DiscordPayload } from "../types/discord.ts"; -import type { +import { DiscordPayload } from "../types/discord.ts"; +import { MessageCreateOptions, MessageDeleteBulkPayload, MessageDeletePayload, @@ -15,9 +15,6 @@ export async function handleInternalMessageCreate(data: DiscordPayload) { const channel = await cacheHandlers.get("channels", payload.channel_id); if (channel) channel.lastMessageID = payload.id; - const message = await structures.createMessage(payload); - // Cache the message - cacheHandlers.set("messages", payload.id, message); const guild = payload.guild_id ? await cacheHandlers.get("guilds", payload.guild_id) : undefined; @@ -46,6 +43,10 @@ export async function handleInternalMessageCreate(data: DiscordPayload) { } }); + const message = await structures.createMessage(payload); + // Cache the message + cacheHandlers.set("messages", payload.id, message); + eventHandlers.messageCreate?.(message); } diff --git a/src/controllers/misc.ts b/src/controllers/misc.ts index 3cd7bc813..54f60991d 100644 --- a/src/controllers/misc.ts +++ b/src/controllers/misc.ts @@ -2,7 +2,7 @@ import { delay } from "../../deps.ts"; import { eventHandlers, setBotID } from "../module/client.ts"; import { allowNextShard } from "../module/shardingManager.ts"; import { structures } from "../structures/mod.ts"; -import type { +import { DiscordPayload, PresenceUpdatePayload, ReadyPayload, @@ -10,7 +10,7 @@ import type { VoiceStateUpdatePayload, WebhookUpdatePayload, } from "../types/discord.ts"; -import type { UserPayload } from "../types/guild.ts"; +import { UserPayload } from "../types/guild.ts"; import { cache } from "../utils/cache.ts"; import { cacheHandlers } from "./cache.ts"; diff --git a/src/controllers/reactions.ts b/src/controllers/reactions.ts index 670a7126d..3e867a9b8 100644 --- a/src/controllers/reactions.ts +++ b/src/controllers/reactions.ts @@ -1,7 +1,7 @@ import { botID, eventHandlers } from "../module/client.ts"; import { structures } from "../structures/mod.ts"; -import type { DiscordPayload } from "../types/discord.ts"; -import type { +import { DiscordPayload } from "../types/discord.ts"; +import { BaseMessageReactionPayload, MessageReactionPayload, MessageReactionRemoveEmojiPayload, diff --git a/src/controllers/roles.ts b/src/controllers/roles.ts index 9f889d23b..fe6718798 100644 --- a/src/controllers/roles.ts +++ b/src/controllers/roles.ts @@ -1,10 +1,7 @@ import { eventHandlers } from "../module/client.ts"; import { structures } from "../structures/mod.ts"; -import type { DiscordPayload } from "../types/discord.ts"; -import type { - GuildRoleDeletePayload, - GuildRolePayload, -} from "../types/guild.ts"; +import { DiscordPayload } from "../types/discord.ts"; +import { GuildRoleDeletePayload, GuildRolePayload } from "../types/guild.ts"; import { cacheHandlers } from "./cache.ts"; export async function handleInternalGuildRoleCreate(data: DiscordPayload) { @@ -30,6 +27,11 @@ export async function handleInternalGuildRoleDelete(data: DiscordPayload) { const cachedRole = guild.roles.get(payload.role_id)!; guild.roles.delete(payload.role_id); eventHandlers.roleDelete?.(guild, cachedRole); + + // For bots without GUILD_MEMBERS member.roles is never updated breaking permissions checking. + guild.members.forEach((member) => { + member.roles = member.roles.filter((id) => id !== payload.role_id); + }); } export async function handleInternalGuildRoleUpdate(data: DiscordPayload) { @@ -43,5 +45,6 @@ export async function handleInternalGuildRoleUpdate(data: DiscordPayload) { if (!cachedRole) return; const role = await structures.createRole(payload.role); + guild.roles.set(payload.role.id, role); eventHandlers.roleUpdate?.(guild, role, cachedRole); } diff --git a/src/handlers/channel.ts b/src/handlers/channel.ts index 881521ea0..37f2e7a9f 100644 --- a/src/handlers/channel.ts +++ b/src/handlers/channel.ts @@ -1,8 +1,10 @@ import { endpoints } from "../constants/discord.ts"; +import { cacheHandlers } from "../controllers/cache.ts"; import { RequestManager } from "../module/requestManager.ts"; import { structures } from "../structures/mod.ts"; -import type { +import { ChannelEditOptions, + ChannelTypes, CreateInviteOptions, FollowedChannelPayload, GetMessages, @@ -12,16 +14,19 @@ import type { MessageContent, } from "../types/channel.ts"; import { Errors } from "../types/errors.ts"; -import type { RawOverwrite } from "../types/guild.ts"; -import type { MessageCreateOptions } from "../types/message.ts"; +import { PermissionOverwrite } from "../types/guild.ts"; +import { MessageCreateOptions } from "../types/message.ts"; import { Permissions } from "../types/permission.ts"; -import { botHasChannelPermissions } from "../utils/permissions.ts"; +import { + botHasChannelPermissions, + calculateBits, +} from "../utils/permissions.ts"; /** Checks if a channel overwrite for a user id or a role id has permission in this channel */ export function channelOverwriteHasPermission( guildID: string, id: string, - overwrites: RawOverwrite[], + overwrites: PermissionOverwrite[], permissions: Permissions[], ) { const overwrite = overwrites.find((perm) => perm.id === id) || @@ -29,8 +34,10 @@ export function channelOverwriteHasPermission( return permissions.every((perm) => { if (overwrite) { - if (BigInt(overwrite.deny) & BigInt(perm)) return false; - if (BigInt(overwrite.allow) & BigInt(perm)) return true; + const allowBits = calculateBits(overwrite.allow); + const denyBits = calculateBits(overwrite.deny); + if (BigInt(denyBits) & BigInt(perm)) return false; + if (BigInt(allowBits) & BigInt(perm)) return true; } return false; }); @@ -160,6 +167,15 @@ export async function sendMessage( } } + const channel = await cacheHandlers.get("channels", channelID); + if (!channel) throw new Error(Errors.CHANNEL_NOT_FOUND); + if ( + ![ChannelTypes.DM, ChannelTypes.GUILD_NEWS, ChannelTypes.GUILD_TEXT] + .includes(channel.type) + ) { + throw new Error(Errors.CHANNEL_NOT_TEXT_BASED); + } + const result = await RequestManager.post( endpoints.CHANNEL_MESSAGES(channelID), { diff --git a/src/handlers/guild.ts b/src/handlers/guild.ts index 5ef7b5f07..f80780993 100644 --- a/src/handlers/guild.ts +++ b/src/handlers/guild.ts @@ -3,13 +3,13 @@ import { cacheHandlers } from "../controllers/cache.ts"; import { identifyPayload } from "../module/client.ts"; import { RequestManager } from "../module/requestManager.ts"; import { requestAllMembers } from "../module/shardingManager.ts"; -import type { Guild } from "../structures/guild.ts"; -import type { Member } from "../structures/member.ts"; +import { Guild } from "../structures/guild.ts"; +import { Member } from "../structures/member.ts"; import { structures } from "../structures/mod.ts"; -import type { ImageFormats, ImageSize } from "../types/cdn.ts"; +import { ImageFormats, ImageSize } from "../types/cdn.ts"; import { ChannelCreatePayload, ChannelTypes } from "../types/channel.ts"; import { Errors } from "../types/errors.ts"; -import type { +import { BannedUser, BanOptions, ChannelCreateOptions, @@ -26,13 +26,13 @@ import type { PrunePayload, UserPayload, } from "../types/guild.ts"; -import type { MemberCreatePayload } from "../types/member.ts"; +import { MemberCreatePayload } from "../types/member.ts"; import { Intents } from "../types/options.ts"; import { Permissions } from "../types/permission.ts"; -import type { RoleData } from "../types/role.ts"; +import { RoleData } from "../types/role.ts"; import { formatImageURL } from "../utils/cdn.ts"; import { Collection } from "../utils/collection.ts"; -import { botHasPermission } from "../utils/permissions.ts"; +import { botHasPermission, calculateBits } from "../utils/permissions.ts"; import { urlToBase64 } from "../utils/utils.ts"; /** Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. This endpoint can be used only by bots in less than 10 guilds. */ @@ -105,7 +105,7 @@ export async function createGuildChannel( (await RequestManager.post(endpoints.GUILD_CHANNELS(guild.id), { ...options, name, - permission_overwrites: options?.permission_overwrites?.map((perm) => ({ + permission_overwrites: options?.permissionOverwrites?.map((perm) => ({ ...perm, allow: perm.allow.reduce( @@ -317,7 +317,12 @@ export function editRole( ) { throw new Error(Errors.MISSING_MANAGE_ROLES); } - return RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), options); + return RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), { + ...options, + permissions: options.permissions + ? calculateBits(options.permissions) + : undefined, + }); } /** Delete a guild role. Requires the MANAGE_ROLES permission. */ diff --git a/src/handlers/member.ts b/src/handlers/member.ts index 9d31cf4af..8b0faf83e 100644 --- a/src/handlers/member.ts +++ b/src/handlers/member.ts @@ -2,15 +2,12 @@ import { endpoints } from "../constants/discord.ts"; import { cacheHandlers } from "../controllers/cache.ts"; import { botID } from "../module/client.ts"; import { RequestManager } from "../module/requestManager.ts"; -import type { Member } from "../structures/member.ts"; +import { Member } from "../structures/member.ts"; import { structures } from "../structures/mod.ts"; -import type { ImageFormats, ImageSize } from "../types/cdn.ts"; -import type { - DMChannelCreatePayload, - MessageContent, -} from "../types/channel.ts"; +import { ImageFormats, ImageSize } from "../types/cdn.ts"; +import { DMChannelCreatePayload, MessageContent } from "../types/channel.ts"; import { Errors } from "../types/errors.ts"; -import type { EditMemberOptions } from "../types/member.ts"; +import { EditMemberOptions } from "../types/member.ts"; import { Permissions } from "../types/permission.ts"; import { formatImageURL } from "../utils/cdn.ts"; import { diff --git a/src/handlers/message.ts b/src/handlers/message.ts index 3b99c5b32..243404125 100644 --- a/src/handlers/message.ts +++ b/src/handlers/message.ts @@ -3,12 +3,12 @@ import { endpoints } from "../constants/discord.ts"; import { cacheHandlers } from "../controllers/cache.ts"; import { botID } from "../module/client.ts"; import { RequestManager } from "../module/requestManager.ts"; -import type { Message } from "../structures/message.ts"; +import { Message } from "../structures/message.ts"; import { structures } from "../structures/mod.ts"; -import type { MessageContent } from "../types/channel.ts"; +import { MessageContent } from "../types/channel.ts"; import { Errors } from "../types/errors.ts"; -import type { UserPayload } from "../types/guild.ts"; -import type { MessageCreateOptions } from "../types/message.ts"; +import { UserPayload } from "../types/guild.ts"; +import { MessageCreateOptions } from "../types/message.ts"; import { Permissions } from "../types/permission.ts"; import { botHasChannelPermissions } from "../utils/permissions.ts"; diff --git a/src/handlers/webhook.ts b/src/handlers/webhook.ts index e4b37509a..7bb9400b3 100644 --- a/src/handlers/webhook.ts +++ b/src/handlers/webhook.ts @@ -2,9 +2,9 @@ import { endpoints } from "../constants/discord.ts"; import { RequestManager } from "../module/requestManager.ts"; import { structures } from "../structures/mod.ts"; import { Errors } from "../types/errors.ts"; -import type { MessageCreateOptions } from "../types/message.ts"; +import { MessageCreateOptions } from "../types/message.ts"; import { Permissions } from "../types/permission.ts"; -import type { +import { ExecuteWebhookOptions, WebhookCreateOptions, WebhookPayload, diff --git a/src/module/basicShard.ts b/src/module/basicShard.ts index d55ad72c5..ee1a9cc04 100644 --- a/src/module/basicShard.ts +++ b/src/module/basicShard.ts @@ -7,21 +7,22 @@ import { isWebSocketPongEvent, WebSocket, } from "../../deps.ts"; -import type { +import { DiscordBotGatewayData, DiscordHeartbeatPayload, + GatewayOpcode, ReadyPayload, } from "../types/discord.ts"; -import { GatewayOpcode } from "../types/discord.ts"; -import type { FetchMembersOptions } from "../types/guild.ts"; -import type { BotStatusRequest } from "../utils/utils.ts"; -import type { IdentifyPayload } from "./client.ts"; -import { botGatewayData, eventHandlers } from "./client.ts"; +import { FetchMembersOptions } from "../types/guild.ts"; +import { BotStatusRequest } from "../utils/utils.ts"; +import { botGatewayData, eventHandlers, IdentifyPayload } from "./client.ts"; import { handleDiscordPayload } from "./shardingManager.ts"; const basicShards = new Map(); -const heartbeating = new Set(); +const heartbeating = new Map(); const utf8decoder = new TextDecoder(); +const RequestMembersQueue: RequestMemberQueuedRequest[] = []; +let processQueue = false; export interface BasicShard { id: number; @@ -32,9 +33,6 @@ export interface BasicShard { needToResume: boolean; } -const RequestMembersQueue: RequestMemberQueuedRequest[] = []; -let processQueue = false; - interface RequestMemberQueuedRequest { guildID: string; shardID: number; @@ -52,7 +50,7 @@ export async function createBasicShard( const basicShard: BasicShard = { id: shardID, - socket: await connectWebSocket(`${data.url}?v=6&encoding=json`), + socket: await connectWebSocket(`${data.url}?v=8&encoding=json`), resumeInterval: 0, sessionID: oldShard?.sessionID || "", previousSequenceNumber: oldShard?.previousSequenceNumber || 0, @@ -123,9 +121,13 @@ export async function createBasicShard( heartbeat( basicShard, (data.d as DiscordHeartbeatPayload).heartbeat_interval, + identifyPayload, ); } break; + case GatewayOpcode.HeartbeatACK: + heartbeating.set(shardID, true); + break; case GatewayOpcode.Reconnect: eventHandlers.debug?.( { type: "reconnect", data: { shardID: basicShard.id } }, @@ -200,17 +202,39 @@ function resume(shard: BasicShard, payload: IdentifyPayload) { })); } -// 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 heartbeat( shard: BasicShard, interval: number, + payload: IdentifyPayload, ) { + // We lost socket connection between heartbeats, resume connection if (shard.socket.isClosed) { + shard.needToResume = true; + resumeConnection(botGatewayData, payload, shard.id); heartbeating.delete(shard.id); return; } - if (!heartbeating.has(shard.id)) heartbeating.add(shard.id); + if (heartbeating.has(shard.id)) { + const receivedACK = heartbeating.get(shard.id); + // If a ACK response was not received since last heartbeat, issue invalid session close + if (!receivedACK) { + eventHandlers.debug?.( + { + type: "heartbeatStopped", + data: { + interval, + previousSequenceNumber: shard.previousSequenceNumber, + shardID: shard.id, + }, + }, + ); + return shard.socket.send(JSON.stringify({ op: 4009 })); + } + } + + // Set it to false as we are issuing a new heartbeat + heartbeating.set(shard.id, false); shard.socket.send( JSON.stringify( @@ -228,7 +252,7 @@ async function heartbeat( }, ); await delay(interval); - heartbeat(shard, interval); + heartbeat(shard, interval, payload); } async function resumeConnection( diff --git a/src/module/client.ts b/src/module/client.ts index 53dbcf843..01f311460 100644 --- a/src/module/client.ts +++ b/src/module/client.ts @@ -1,6 +1,6 @@ import { endpoints } from "../constants/discord.ts"; -import type { DiscordBotGatewayData } from "../types/discord.ts"; -import type { ClientOptions, EventHandlers } from "../types/options.ts"; +import { DiscordBotGatewayData } from "../types/discord.ts"; +import { ClientOptions, EventHandlers } from "../types/options.ts"; import { RequestManager } from "./requestManager.ts"; import { spawnShards } from "./shardingManager.ts"; diff --git a/src/module/requestManager.ts b/src/module/requestManager.ts index 8d37a14fb..dbbdabd15 100644 --- a/src/module/requestManager.ts +++ b/src/module/requestManager.ts @@ -2,7 +2,7 @@ import { delay } from "../../deps.ts"; import { baseEndpoints } from "../constants/discord.ts"; import { HttpResponseCode } from "../types/discord.ts"; import { Errors } from "../types/errors.ts"; -import type { RequestMethods } from "../types/fetch.ts"; +import { RequestMethods } from "../types/fetch.ts"; import { authorization, eventHandlers } from "./client.ts"; const pathQueues: { [key: string]: QueuedRequest[] } = {}; @@ -286,6 +286,23 @@ async function runMethod( }); } +async function logErrors(response: Response, errorStack?: unknown) { + try { + const error = await response.json(); + console.error(error); + + eventHandlers.debug?.({ type: "error", data: { errorStack, error } }); + } catch { + eventHandlers.debug?.( + { + type: "error", + data: { errorStack }, + }, + ); + console.error(response); + } +} + function handleStatusCode(response: Response, errorStack?: unknown) { const status = response.status; @@ -296,13 +313,7 @@ function handleStatusCode(response: Response, errorStack?: unknown) { return true; } - eventHandlers.debug?.( - { - type: "error", - data: { errorStack }, - }, - ); - console.error(response); + logErrors(response, errorStack); switch (status) { case HttpResponseCode.BadRequest: diff --git a/src/module/shard.ts b/src/module/shard.ts index c2e864487..8e7353127 100644 --- a/src/module/shard.ts +++ b/src/module/shard.ts @@ -1,13 +1,17 @@ -import type { WebSocket } from "../../deps.ts"; -import { connectWebSocket, delay, isWebSocketCloseEvent } from "../../deps.ts"; -import type { +import { + connectWebSocket, + delay, + isWebSocketCloseEvent, + WebSocket, +} from "../../deps.ts"; +import { DiscordBotGatewayData, DiscordHeartbeatPayload, + GatewayOpcode, ReadyPayload, } from "../types/discord.ts"; -import { GatewayOpcode } from "../types/discord.ts"; -import type { FetchMembersOptions } from "../types/guild.ts"; -import type { DebugArg } from "../types/options.ts"; +import { FetchMembersOptions } from "../types/guild.ts"; +import { DebugArg } from "../types/options.ts"; let shardSocket: WebSocket; diff --git a/src/module/shardingManager.ts b/src/module/shardingManager.ts index 4b1cb2d31..e95ed6c0d 100644 --- a/src/module/shardingManager.ts +++ b/src/module/shardingManager.ts @@ -1,21 +1,25 @@ import { delay } from "../../deps.ts"; import { controllers } from "../controllers/mod.ts"; -import type { Guild } from "../structures/guild.ts"; -import type { +import { Guild } from "../structures/guild.ts"; +import { DiscordBotGatewayData, DiscordPayload, + GatewayOpcode, } from "../types/discord.ts"; -import { GatewayOpcode } from "../types/discord.ts"; -import type { FetchMembersOptions } from "../types/guild.ts"; +import { FetchMembersOptions } from "../types/guild.ts"; import { cache } from "../utils/cache.ts"; -import type { BotStatusRequest } from "../utils/utils.ts"; +import { BotStatusRequest } from "../utils/utils.ts"; import { botGatewayStatusRequest, createBasicShard, requestGuildMembers, } from "./basicShard.ts"; -import type { IdentifyPayload } from "./client.ts"; -import { botGatewayData, eventHandlers, identifyPayload } from "./client.ts"; +import { + botGatewayData, + eventHandlers, + IdentifyPayload, + identifyPayload, +} from "./client.ts"; let shardCounter = 0; let basicSharding = false; diff --git a/src/structures/channel.ts b/src/structures/channel.ts index 64e77ef9b..b36662d2b 100644 --- a/src/structures/channel.ts +++ b/src/structures/channel.ts @@ -1,6 +1,7 @@ import { cacheHandlers } from "../controllers/cache.ts"; -import type { ChannelCreatePayload } from "../types/channel.ts"; -import type { Unpromise } from "../types/misc.ts"; +import { ChannelCreatePayload } from "../types/channel.ts"; +import { PermissionOverwrite } from "../types/guild.ts"; +import { Unpromise } from "../types/misc.ts"; import { calculatePermissions } from "../utils/permissions.ts"; export async function createChannel( @@ -14,13 +15,14 @@ export async function createChannel( rate_limit_per_user: rateLimitPerUser, parent_id: parentID, last_pin_timestamp: lastPinTimestamp, + permission_overwrites, ...rest } = data; const channel = { ...rest, /** The guild id of the channel if it is a guild channel. */ - guildID: guildID || rawGuildID, + guildID: guildID || rawGuildID || "", /** The id of the last message sent in this channel */ lastMessageID, /** The amount of users allowed in this voice channel. */ @@ -32,13 +34,14 @@ export async function createChannel( /** The last time when a message was pinned in this channel */ lastPinTimestamp, /** The permission overwrites for this channel */ - permissions: data.permission_overwrites - ? data.permission_overwrites.map((perm) => ({ - ...perm, - allow: calculatePermissions(BigInt(perm.allow)), - deny: calculatePermissions(BigInt(perm.deny)), - })) - : [], + permissionOverwrites: + (data.permission_overwrites + ? data.permission_overwrites.map((perm) => ({ + ...perm, + allow: calculatePermissions(BigInt(perm.allow)), + deny: calculatePermissions(BigInt(perm.deny)), + })) + : []) as PermissionOverwrite[], /** Whether this channel is nsfw or not */ nsfw: data.nsfw || false, /** The mention of the channel */ diff --git a/src/structures/guild.ts b/src/structures/guild.ts index 70f391c7f..3cde3638f 100644 --- a/src/structures/guild.ts +++ b/src/structures/guild.ts @@ -1,7 +1,7 @@ -import type { CreateGuildPayload } from "../types/guild.ts"; -import type { Unpromise } from "../types/misc.ts"; +import { CreateGuildPayload } from "../types/guild.ts"; +import { Unpromise } from "../types/misc.ts"; import { Collection } from "../utils/collection.ts"; -import type { Member } from "./member.ts"; +import { Member } from "./member.ts"; import { structures } from "./mod.ts"; export async function createGuild(data: CreateGuildPayload, shardID: number) { diff --git a/src/structures/member.ts b/src/structures/member.ts index a62391876..97e91e832 100644 --- a/src/structures/member.ts +++ b/src/structures/member.ts @@ -1,5 +1,5 @@ -import type { MemberCreatePayload } from "../types/member.ts"; -import type { Unpromise } from "../types/misc.ts"; +import { MemberCreatePayload } from "../types/member.ts"; +import { Unpromise } from "../types/misc.ts"; export async function createMember(data: MemberCreatePayload, guildID: string) { const { diff --git a/src/structures/message.ts b/src/structures/message.ts index bd41e48ed..3c2fab5e5 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -1,5 +1,5 @@ -import type { MessageCreateOptions } from "../types/message.ts"; -import type { Unpromise } from "../types/misc.ts"; +import { MessageCreateOptions } from "../types/message.ts"; +import { Unpromise } from "../types/misc.ts"; export async function createMessage(data: MessageCreateOptions) { const { diff --git a/src/structures/role.ts b/src/structures/role.ts index 6aaba4e5a..8f8f87e3d 100644 --- a/src/structures/role.ts +++ b/src/structures/role.ts @@ -1,5 +1,5 @@ -import type { Unpromise } from "../types/misc.ts"; -import type { RoleData } from "../types/role.ts"; +import { Unpromise } from "../types/misc.ts"; +import { RoleData } from "../types/role.ts"; export async function createRole(data: RoleData) { return { diff --git a/src/types/activity.ts b/src/types/activity.ts index 81a180b4b..cc8110d87 100644 --- a/src/types/activity.ts +++ b/src/types/activity.ts @@ -1,12 +1,18 @@ -import type { Timestamps } from "./discord.ts"; - export interface ActivityPayload { name: string; type: number; url?: string; created_at: number; - timestamps: Timestamps; + timestamps?: ActivityTimestamps; + application_id?: string; details?: string; + state?: string; + emoji?: ActivityEmoji; + party?: ActivityParty; + assets?: ActivityAssets; + secrets?: ActivitySecrets; + instance?: boolean; + flags?: number; } export enum ActivityType { @@ -18,4 +24,44 @@ export enum ActivityType { Listening, /** Example: ":smiley: I am cool" */ Custom = 4, + /** Example: "Competing in Arena World Champions" */ + Competing, +} + +export interface ActivityTimestamps { + start?: number; + end?: number; +} + +export interface ActivityEmoji { + name: string; + id?: string; + animated?: boolean; +} + +export interface ActivityParty { + id?: string; + size?: [number, number]; +} + +export interface ActivityAssets { + large_image?: string; + large_text?: string; + small_image?: string; + small_text?: string; +} + +export interface ActivitySecrets { + join?: string; + spectate?: string; + match?: string; +} + +export enum ActivityFlags { + INSTANCE = 1 << 0, + JOIN = 1 << 1, + SPECTATE = 1 << 2, + JOIN_REQUEST = 1 << 3, + SYNC = 1 << 4, + PLAY = 1 << 5, } diff --git a/src/types/channel.ts b/src/types/channel.ts index f4892bad2..d81d8c810 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -1,5 +1,5 @@ -import type { Overwrite, RawOverwrite } from "./guild.ts"; -import type { Embed } from "./message.ts"; +import { Overwrite, RawOverwrite } from "./guild.ts"; +import { Embed } from "./message.ts"; export interface ChannelEditOptions { /** 2-100 character channel name. All */ diff --git a/src/types/discord.ts b/src/types/discord.ts index 161c15b59..d750d9e24 100644 --- a/src/types/discord.ts +++ b/src/types/discord.ts @@ -1,7 +1,7 @@ -import type { PartialUser, UserPayload } from "./guild.ts"; -import type { MemberCreatePayload } from "./member.ts"; -import type { Activity } from "./message.ts"; -import type { ClientStatusPayload } from "./presence.ts"; +import { PartialUser, UserPayload } from "./guild.ts"; +import { MemberCreatePayload } from "./member.ts"; +import { Activity } from "./message.ts"; +import { ClientStatusPayload } from "./presence.ts"; export interface DiscordPayload { /** OP code for the payload */ @@ -196,11 +196,6 @@ export interface Properties { $device: string; } -export interface Timestamps { - start?: number; - end?: number; -} - export interface Emoji { name: string; id?: string; diff --git a/src/types/errors.ts b/src/types/errors.ts index da321ff35..98df112d2 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -31,4 +31,6 @@ export enum Errors { CHANNEL_NOT_IN_GUILD = "CHANNEL_NOT_IN_GUILD", INVALID_WEBHOOK_NAME = "INVALID_WEBHOOK_NAME", INVALID_WEBHOOK_OPTIONS = "INVALID_WEBHOOK_OPTIONS", + CHANNEL_NOT_FOUND = "CHANNEL_NOT_FOUND", + CHANNEL_NOT_TEXT_BASED = "CHANNEL_NOT_TEXT_BASED", } diff --git a/src/types/guild.ts b/src/types/guild.ts index 8e35d70d2..ba52fedfd 100644 --- a/src/types/guild.ts +++ b/src/types/guild.ts @@ -1,10 +1,10 @@ -import type { ChannelCreatePayload, ChannelTypes } from "./channel.ts"; -import type { Emoji, StatusType } from "./discord.ts"; -import type { MemberCreatePayload } from "./member.ts"; -import type { Activity } from "./message.ts"; -import type { Permission } from "./permission.ts"; -import type { ClientStatusPayload } from "./presence.ts"; -import type { RoleData } from "./role.ts"; +import { ChannelCreatePayload, ChannelTypes } from "./channel.ts"; +import { Emoji, StatusType } from "./discord.ts"; +import { MemberCreatePayload } from "./member.ts"; +import { Activity } from "./message.ts"; +import { Permission } from "./permission.ts"; +import { ClientStatusPayload } from "./presence.ts"; +import { RoleData } from "./role.ts"; export interface GuildRolePayload { /** The id of the guild */ @@ -473,6 +473,12 @@ export interface RawOverwrite { deny: number; } +export interface PermissionOverwrite + extends Omit { + allow: Permission[]; + deny: Permission[]; +} + export interface ChannelCreateOptions { /** The type of the channel */ type?: ChannelTypes; @@ -487,7 +493,7 @@ export interface ChannelCreateOptions { /** The sorting position of the channel */ position?: number; /** The channel's permission overwrites */ - permission_overwrites?: Overwrite[]; + permissionOverwrites?: Overwrite[]; /** The id of the parent category for the channel */ parent_id?: string; /** Whether the channel is nsfw */ diff --git a/src/types/member.ts b/src/types/member.ts index a68f7c887..199e0fef5 100644 --- a/src/types/member.ts +++ b/src/types/member.ts @@ -1,4 +1,4 @@ -import type { UserPayload } from "./guild.ts"; +import { UserPayload } from "./guild.ts"; export interface EditMemberOptions { /** Value to set users nickname to. Requires MANAGE_NICKNAMES permission. */ diff --git a/src/types/message.ts b/src/types/message.ts index 8bb2fdd18..1e631d3cc 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -1,7 +1,7 @@ -import type { Channel } from "../structures/channel.ts"; -import type { ChannelType } from "./channel.ts"; -import type { UserPayload } from "./guild.ts"; -import type { MemberCreatePayload } from "./member.ts"; +import { Channel } from "../structures/channel.ts"; +import { ChannelType } from "./channel.ts"; +import { UserPayload } from "./guild.ts"; +import { MemberCreatePayload } from "./member.ts"; export interface MentionedUser extends UserPayload { member: MemberCreatePayload; diff --git a/src/types/options.ts b/src/types/options.ts index 4b57be9f4..f3c66fc01 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -1,9 +1,9 @@ -import type { Channel } from "../structures/channel.ts"; -import type { Guild } from "../structures/guild.ts"; -import type { Member } from "../structures/member.ts"; -import type { Message } from "../structures/message.ts"; -import type { Role } from "../structures/role.ts"; -import type { +import { Channel } from "../structures/channel.ts"; +import { Guild } from "../structures/guild.ts"; +import { Member } from "../structures/member.ts"; +import { Message } from "../structures/message.ts"; +import { Role } from "../structures/role.ts"; +import { DiscordPayload, Emoji, PresenceUpdatePayload, @@ -11,8 +11,8 @@ import type { TypingStartPayload, VoiceStateUpdatePayload, } from "./discord.ts"; -import type { UserPayload } from "./guild.ts"; -import type { +import { UserPayload } from "./guild.ts"; +import { Attachment, BaseMessageReactionPayload, Embed, @@ -64,6 +64,7 @@ export interface DebugArg { | "requestManagerFetched" | "requestMembersProcessing" | "heartbeat" + | "heartbeatStopped" | "createShard" | "invalidSession" | "reconnect" diff --git a/src/types/presence.ts b/src/types/presence.ts index 1efc05220..e2ebd6f73 100644 --- a/src/types/presence.ts +++ b/src/types/presence.ts @@ -1,4 +1,4 @@ -import type { StatusType } from "./discord.ts"; +import { StatusType } from "./discord.ts"; export interface ClientStatusPayload { /** The user's status set for an active desktop (Windows, Linux, Mac) application session */ diff --git a/src/types/webhook.ts b/src/types/webhook.ts index 81adb0c13..8974e6575 100644 --- a/src/types/webhook.ts +++ b/src/types/webhook.ts @@ -1,5 +1,5 @@ -import type { UserPayload } from "./guild.ts"; -import type { Embed } from "./message.ts"; +import { UserPayload } from "./guild.ts"; +import { Embed } from "./message.ts"; export interface WebhookPayload { /** The id of the webhook */ diff --git a/src/utils/cache.ts b/src/utils/cache.ts index 3bb3c8d26..dbd89b898 100644 --- a/src/utils/cache.ts +++ b/src/utils/cache.ts @@ -1,7 +1,7 @@ -import type { Channel } from "../structures/channel.ts"; -import type { Guild } from "../structures/guild.ts"; -import type { Message } from "../structures/message.ts"; -import type { PresenceUpdatePayload } from "../types/discord.ts"; +import { Channel } from "../structures/channel.ts"; +import { Guild } from "../structures/guild.ts"; +import { Message } from "../structures/message.ts"; +import { PresenceUpdatePayload } from "../types/discord.ts"; import { Collection } from "./collection.ts"; export interface CacheData { @@ -21,5 +21,5 @@ export const cache: CacheData = { messages: new Collection(), unavailableGuilds: new Collection(), presences: new Collection(), - fetchAllMembersProcessingRequests: new Collection(), + fetchAllMembersProcessingRequests: new Collection(), }; diff --git a/src/utils/cdn.ts b/src/utils/cdn.ts index b4748e219..6c5275ac5 100644 --- a/src/utils/cdn.ts +++ b/src/utils/cdn.ts @@ -1,4 +1,4 @@ -import type { ImageFormats, ImageSize } from "../types/cdn.ts"; +import { ImageFormats, ImageSize } from "../types/cdn.ts"; export const formatImageURL = ( url: string, diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 013c1c56c..2537f3cf2 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -1,9 +1,9 @@ import { cacheHandlers } from "../controllers/cache.ts"; import { botID } from "../module/client.ts"; -import type { Guild } from "../structures/guild.ts"; -import type { Role } from "../structures/role.ts"; -import type { Permission } from "../types/permission.ts"; -import { Permissions } from "../types/permission.ts"; +import { Guild } from "../structures/guild.ts"; +import { Role } from "../structures/role.ts"; +import { PermissionOverwrite } from "../types/guild.ts"; +import { Permission, Permissions } from "../types/permission.ts"; /** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */ export async function memberIDHasPermission( @@ -34,6 +34,8 @@ export function memberHasPermission( const permissionBits = memberRoleIDs.map((id) => guild.roles.get(id)?.permissions ) + // Removes any edge case undefined + .filter((id) => id) .reduce((bits, permissions) => { bits |= BigInt(permissions); return bits; @@ -58,6 +60,8 @@ export async function botHasPermission( const permissionBits = member.roles .map((id) => guild.roles.get(id)!) + // Remove any edge case undefined + .filter((r) => r) .reduce((bits, data) => { bits |= BigInt(data.permissions); @@ -84,101 +88,104 @@ export async function hasChannelPermissions( permissions: Permissions[], ) { const channel = await cacheHandlers.get("channels", channelID); - if (!channel?.guildID) return true; + if (!channel) return false; + if (!channel.guildID) return true; const guild = await cacheHandlers.get("guilds", channel.guildID); if (!guild) return false; if (guild.ownerID === memberID) return true; - if (botHasPermission(guild.id, [Permissions.ADMINISTRATOR])) return true; + if ( + await memberIDHasPermission(memberID, guild.id, ["ADMINISTRATOR"]) + ) { + return true; + } const member = guild.members.get(memberID); if (!member) return false; - const memberOverwrite = channel.permission_overwrites?.find((o) => - o.id === memberID - ); + let memberOverwrite: PermissionOverwrite | undefined; + let everyoneOverwrite: PermissionOverwrite | undefined; + let rolesOverwrites: PermissionOverwrite[] = []; - const rolesOverwrites = channel.permission_overwrites?.filter((o) => - member.roles.includes(o.id) - ); - - const everyoneOverwrite = channel.permission_overwrites?.find((o) => - o.id === guild.id - ); + for (const overwrite of channel.permissionOverwrites || []) { + // If the overwrite on this channel is specific to this member + if (overwrite.id === memberID) memberOverwrite = overwrite; + // If it is the everyone role overwrite + if (overwrite.id === guild.id) everyoneOverwrite = overwrite; + // If it is one of the roles the member has + if (member.roles.includes(overwrite.id)) rolesOverwrites.push(overwrite); + } const allowedPermissions = new Set(); + // Member perms override everything so we must check them first if (memberOverwrite) { - // One of the necessary permissions is denied - if ( - permissions.some((perm) => BigInt(memberOverwrite.deny) & BigInt(perm)) - ) { - return false; - } - permissions.forEach((perm) => { + const allowBits = calculateBits(memberOverwrite.allow); + const denyBits = calculateBits(memberOverwrite.deny); + for (const perm of permissions) { + // One of the necessary permissions is denied. Since this is main permission we can cancel if its denied. + if (BigInt(denyBits) & BigInt(perm)) return false; // Already allowed perm - if (allowedPermissions.has(perm)) return; + if (allowedPermissions.has(perm)) continue; + // This perm is allowed so we save it - if (BigInt(memberOverwrite.allow) & BigInt(perm)) { + if (BigInt(allowBits) & BigInt(perm)) { allowedPermissions.add(perm); } - }); + } } // Check the necessary permissions for roles - if (rolesOverwrites?.length) { - if ( - rolesOverwrites.some((overwrite) => - permissions.some((perm) => - (BigInt(overwrite.deny) & BigInt(perm)) && - // If another role allows these perms then they are not denied - !rolesOverwrites.some((o) => BigInt(o.allow) & BigInt(perm)) && - // Make sure the memberOverwrite does not allow this perm - !(memberOverwrite && BigInt(memberOverwrite.allow) & BigInt(perm)) - ) - ) - ) { - return false; - } + for (const perm of permissions) { + // If this is already allowed, skip + if (allowedPermissions.has(perm)) continue; - permissions.forEach((perm) => { + for (const overwrite of rolesOverwrites) { + const allowBits = calculateBits(overwrite.allow); + // This perm is allowed so we save it + if (BigInt(allowBits) & BigInt(perm)) { + allowedPermissions.add(perm); + break; + } + + const denyBits = calculateBits(overwrite.deny); + // If this role denies it we need to save and check if another role allows it, allows > deny + if (BigInt(denyBits) & BigInt(perm)) { + // This role denies his perm, but before denying we need to check all other roles if any allow as allow > deny + const isAllowed = rolesOverwrites.some((o) => + BigInt(calculateBits(o.allow)) & BigInt(perm) + ); + if (isAllowed) continue; + // This permission is in fact denied. Since Roles overrule everything below here we can cancel ou here + return false; + } + } + } + + if (everyoneOverwrite) { + const allowBits = calculateBits(everyoneOverwrite.allow); + const denyBits = calculateBits(everyoneOverwrite.deny); + for (const perm of permissions) { // Already allowed perm - if (allowedPermissions.has(perm)) return; - rolesOverwrites.forEach((overwrite) => { - // This perm is allowed so we save it - if (BigInt(overwrite.allow) & BigInt(perm)) { - allowedPermissions.add(perm); - } - }); - }); - } - - // Check the necessary permissions for everyone - if ( - everyoneOverwrite - ) { - if ( - permissions.some((perm) => - BigInt(everyoneOverwrite.deny) & BigInt(perm) && - !allowedPermissions.has(perm) - ) - ) { - return false; - } - // If all permissions are granted - if ( - permissions.every((perm) => - BigInt(everyoneOverwrite.allow) & BigInt(perm) - ) - ) { - return true; + if (allowedPermissions.has(perm)) continue; + // One of the necessary permissions is denied. Since everyone overwrite overrides role perms we can cancel here + if (BigInt(denyBits) & BigInt(perm)) return false; + // This perm is allowed so we save it + if (BigInt(allowBits) & BigInt(perm)) { + allowedPermissions.add(perm); + } } } + // Is there any remaining permission to check role perms or can we determine that permissions are allowed + if (permissions.every((perm) => allowedPermissions.has(perm))) return true; + + // Some permission was not explicitly allowed so we default to checking role perms directly return botHasPermission(guild.id, permissions); } +/** This function converts a bitwise string to permission strings */ export function calculatePermissions(permissionBits: bigint) { return Object.keys(Permissions).filter((perm) => { if (typeof perm !== "number") return false; @@ -186,6 +193,14 @@ export function calculatePermissions(permissionBits: bigint) { }) as Permission[]; } +/** This function converts an array of permissions into the bitwise string. */ +export function calculateBits(permissions: Permission[]) { + return permissions.reduce( + (bits, perm) => bits |= BigInt(Permissions[perm]), + BigInt(0), + ).toString(); +} + export async function highestRole(guildID: string, memberID: string) { const guild = await cacheHandlers.get("guilds", guildID); if (!guild) return; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 78591d522..c73a6033f 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,7 +1,7 @@ import { encode } from "../../deps.ts"; import { sendGatewayCommand } from "../module/shardingManager.ts"; import { ActivityType } from "../types/activity.ts"; -import type { StatusType } from "../types/discord.ts"; +import { StatusType } from "../types/discord.ts"; export const sleep = (timeout: number) => { return new Promise((resolve) => setTimeout(resolve, timeout)); diff --git a/tests/mod.test.ts b/tests/mod.test.ts new file mode 100644 index 000000000..849b13df8 --- /dev/null +++ b/tests/mod.test.ts @@ -0,0 +1,253 @@ +import { assert, assertEquals, delay } from "../deps.ts"; +import { + botID, + cache, + Channel, + createClient, + createGuildChannel, + createGuildRole, + createServer, + deleteChannel, + deleteRole, + deleteServer, + editRole, + getMessage, + Guild, + Intents, + OverwriteType, + Role, + sendMessage, +} from "../mod.ts"; +import { + channelOverwriteHasPermission, + editChannel, +} from "../src/handlers/channel.ts"; +import { getChannel } from "../src/handlers/guild.ts"; +import { Permissions } from "../src/types/permission.ts"; + +const token = Deno.env.get("DISCORD_TOKEN"); +if (!token) throw "Token is not provided"; + +createClient({ + token, + intents: [Intents.GUILD_MESSAGES, Intents.GUILDS], +}); + +// Default options for all test cases +const testOptions = { + sanitizeOps: false, + sanitizeResources: false, +}; + +Deno.test({ + name: "connect to the gateway", + fn: async () => { + // Delay the execution by 15 seconds (15000 ms) + await delay(15000); + + // Check whether botID is nil or not + assert(botID); + }, + ...testOptions, +}); + +const data = { + guildID: "", + roleID: "", + channelID: "", +}; + +Deno.test({ + name: "create a guild", + async fn() { + // Create a guild "Discordeno Test" + const createdGuild = (await createServer({ + name: "Discordeno Test", + })) as Guild; + + // Check whether createdGuild is nil or not + assert(createdGuild); + + data.guildID = createdGuild.id; + }, + ...testOptions, +}); + +// Role + +Deno.test({ + name: "create a role in a guild", + async fn() { + // Create a role "Role 1" in the guild "Discordeno Test" + const createdRole = await createGuildRole(data.guildID, { + name: "Role 1", + }); + + // Check whether the created role is nil or not + assert(createdRole); + + data.roleID = createdRole.id; + }, + ...testOptions, +}); + +Deno.test({ + name: "edit a role in a guild", + async fn() { + // Edit a role "Role 1" in the guild "Discordeno Test" + const editedRole = (await editRole(data.guildID, data.roleID, { + name: "Edited Role", + color: 4320244, + hoist: false, + mentionable: false, + })) as Role; + + // Assertions + assert(editedRole); + assertEquals(editedRole.name, "Edited Role"); + assertEquals(editedRole.color, 4320244); + assertEquals(editedRole.hoist, false); + assertEquals(editedRole.mentionable, false); + + data.roleID = editedRole.id; + }, + ...testOptions, +}); + +// Channel + +Deno.test({ + name: "create a channel in a guild", + async fn() { + const guild = cache.guilds.get(data.guildID); + if (!guild) throw "Guild not found"; + const createdChannel = await createGuildChannel(guild, "test"); + + // Check whether the created channel is nil or not + assert(createdChannel); + + data.channelID = createdChannel.id; + }, + ...testOptions, +}); + +Deno.test({ + name: "get a channel in a guild", + async fn() { + const channel = await getChannel(data.channelID); + + assertEquals(channel.id, data.channelID); + }, + ...testOptions, +}); + +Deno.test({ + name: "edit a channel in a guild", + async fn() { + const channel = await editChannel(data.channelID, { + name: "edited channel", + overwrites: [ + { + id: data.roleID, + type: OverwriteType.ROLE, + allow: ["VIEW_CHANNEL", "SEND_MESSAGES"], + deny: ["USE_EXTERNAL_EMOJIS"], + }, + ], + }) as Channel; + const editedChannel = await getChannel(data.channelID); + + assert(channel); + assertEquals(editedChannel.name, "edited channel"); + }, +}); + +Deno.test({ + name: "channel overwrite has permission", + async fn() { + const channel = cache.channels.get(data.channelID); + if (!channel) throw "Channel not found"; + + if (!channel.permissionOverwrites) throw "Channel overwrites not found."; + + const hasPerm = channelOverwriteHasPermission( + data.guildID, + data.roleID, + channel.permissionOverwrites, + [Permissions.VIEW_CHANNEL, Permissions.SEND_MESSAGES], + ); + const missingPerm = channelOverwriteHasPermission( + data.guildID, + data.roleID, + channel.permissionOverwrites, + [Permissions.USE_EXTERNAL_EMOJIS], + ); + + assertEquals(hasPerm, true); + assertEquals(missingPerm, false); + }, + ...testOptions, +}); + +// Message + +let messageID: string; + +Deno.test({ + name: "create a message in a guild", + async fn() { + const createdMessage = await sendMessage(data.channelID, "test"); + + // Check whether the created message is nil or not + assert(createdMessage); + + messageID = createdMessage.id; + }, +}); + +Deno.test({ + name: "get a message in a guild", + async fn() { + const message = await getMessage(data.channelID, messageID); + + assertEquals(messageID, message.id); + }, +}); + +// Clean up + +Deno.test({ + name: "delete a role from the guild", + async fn() { + await deleteRole(data.guildID, data.roleID); + data.roleID = ""; + assertEquals(data.roleID, ""); + }, +}); + +Deno.test({ + name: "delete a channel in the guild", + async fn() { + await deleteChannel(data.guildID, data.channelID); + }, + ...testOptions, +}); + +Deno.test({ + name: "delete a guild", + async fn() { + await deleteServer(data.guildID); + data.guildID = ""; + assertEquals(data.guildID, ""); + }, + ...testOptions, +}); + +// This is meant to be the final test that forcefully crashes the bot +Deno.test({ + name: "exit the process forcefully after all the tests are done", + async fn() { + Deno.exit(1); + }, + ...testOptions, +});