Merge branch 'master' of https://github.com/Skillz4Killz/Discordeno into large-bot-optimizations

This commit is contained in:
Skillz
2020-11-17 19:27:08 -05:00
55 changed files with 1571 additions and 542 deletions

View File

@@ -1,35 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: Testing/Linting
# 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]
pull_request:
branches: [master]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# 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 Deno environment
uses: denolib/setup-deno@master
- name: Deno Fetch
run: deno cache mod.ts
- name: Deno Format Check
run: deno fmt *.ts --check
- name: Deno Format Check src/
run: deno fmt src/* --check

View File

@@ -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"

16
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Lint
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: denolib/setup-deno@master
- name: Cache the dependencies
run: deno cache mod.ts
- name: Run format script with --check
run: deno fmt --ignore=./docs --check

View File

@@ -1,8 +1,8 @@
name: Ship Nest.Land
on:
release:
types: [published]
create:
ref_type: "tag"
jobs:
release:
@@ -16,5 +16,6 @@ jobs:
- name: Publish module
run: |
deno run -A --unstable https://x.nest.land/eggs@0.2.1/mod.ts link ${{ secrets.NESTAPIKEY }}
deno run -A --unstable https://x.nest.land/eggs@0.2.1/mod.ts publish --version ${{ github.event.inputs.tags }}
deno install -A --unstable https://x.nest.land/eggs@0.3.2/eggs.ts
eggs link ${{ secrets.NESTAPIKEY }}
eggs publish --yes --no-check --version $(git describe --tags $(git rev-list --tags --max-count=1))

17
.github/workflows/test.yml vendored Normal file
View File

@@ -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 }}

View File

@@ -4,5 +4,8 @@
"deno.import_intellisense_origins": {
"https://deno.land": true
},
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"editor.defaultFormatter": "denoland.vscode-deno"
}

View File

@@ -2,61 +2,32 @@
> Discord API library wrapper in Deno
[![Discord](https://img.shields.io/discord/223909216866402304?color=7289da&logo=discord&logoColor=white)](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)
[![Discord](https://img.shields.io/discord/223909216866402304?color=7289da&logo=discord&logoColor=dark)](https://discord.gg/J4NqJ72)
![Lint](https://github.com/Skillz4Killz/Discordeno/workflows/Lint/badge.svg)
![Test](https://github.com/Skillz4Killz/Discordeno/workflows/Test/badge.svg)
[![nest badge](https://nest.land/badge.svg)](https://nest.land/package/Discordeno)
[Website](https://discordeno.netlify.app)
## Why Discordeno?
## Beginner Developers
### 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. 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!
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 with pre-existing libraries and issues that I had when I first started out coding bots.
If you are a beginner developer, you may check out these awesome official and unofficial boilerplates:
**Modular commands, arguments, events, inhibitors, monitors, tasks.**
- Official Discordeno Boilerplate
- [GitHub](https://github.com/Skillz4Killz/Discordeno-bot-template)
- [Features](https://github.com/Skillz4Killz/Discordeno-bot-template#features)
- Clean and powerful commands system
- Powerful argument handling including validating, parsing and modifications.
- Easily create custom arguments for your specific needs.
- Command aliases.
- Cooldowns and allowed uses before cooldown triggers.
- Author and bot permission checks in server AND in channel!
- Clean and powerful events system
- Simple functions that are called when an event occurs.
- Easily reloadable!
- No possible memory leaks due to incorrect EventEmitter usage!
- Useful events available to help debug!
- Clean and powerful inhibitors system
- Stops a command from running if a requirement fails.
- Easily add custom inhibitors!
- Clean and powerful monitors system.
- Runs a function on every message sent. Useful for stuff like auto-moderation or tags.
- Easily ignore bots, users, edits, dms.
- Powerful permission checks.
- Clean and powerful tasks system.
- Runs a function at a certain interval. Useful for things like unmute and updating bot lists etc.
- Can be used for cache sweeping to keep your cache optimized for exactly what you want.
- Botlists code already made for most botlists. Just add your api tokens for each site and magic!
- Clean and powerful languages system.
- Built in multi-lingual support.
- Uses i18next, one of the best localization tools available.
- Supports nested folders to keep cleaner translation files
If you do not wish to use a boilerplate, you may continue reading.
**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!
## Advanced Developers
### Advanced Developers
The instructions below are meant for advanced developers!
Starting with Discordeno is very simple, you can start from scratch without any boilerplates/frameworks: Add this snippet of code into a new TypeScript file:
```typescript
import StartBot, { sendMessage, Intents } from "https://x.nest.land/Discordeno@9.0.1/mod.ts";
import StartBot, { sendMessage, Intents } from "https://x.nest.land/Discordeno@9.0.15/mod.ts";
import config from "./config.ts";
StartBot({
@@ -64,7 +35,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 +48,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](https://github.com/Skillz4Killz/Discordeno/blob/master/LICENSE)

11
deps.ts
View File

@@ -1,5 +1,3 @@
export { delay } from "https://deno.land/std@0.67.0/async/delay.ts";
export { encode } from "https://deno.land/std@0.67.0/encoding/base64.ts";
export {
connectWebSocket,
isWebSocketCloseEvent,
@@ -7,4 +5,11 @@ 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 { decompress_with as inflate } from "https://unpkg.com/@evan/wasm@0.0.11/target/zlib/deno.js";
export { delay } from "https://deno.land/std@0.75.0/async/delay.ts";
export { encode } from "https://deno.land/std@0.75.0/encoding/base64.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.12/target/zlib/deno.js";

View File

@@ -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<string, Command>(),
@@ -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";

View File

@@ -90,7 +90,7 @@ Web-Mystery Tutorials:
- <a href="https://web-mystery.com/articles/running-discord-bot-written-deno-docker" target="_blank">Running a Discord bot written in Deno in Docker</a>
YouTube Tutorials:
- Coming soon to [NTM Development](https://www.youtube.com/channel/UCkOFck-WCQtolha4NJuK7zA/)
- [Discordeno Bot Tutorials YouTube series](https://youtu.be/rIph9-BGsuQ)
---

View File

@@ -113,7 +113,7 @@ Oh my god! You now have a bot with a bunch of features already! You don't believ
3. Run the script below:
```shell
deno run --allow-net --allow-read mod.ts
deno run --allow-net --allow-read --no-check --config tsconfig.json mod.ts
```
The first time you run it, you may see a lot of files being loaded. This is preparing all the magic behind the scene. Once it is ready, you will see something like this:

View File

@@ -66,7 +66,7 @@ Let's add a custom description to our invite command.
🎉 It's that simple. So let's restart the bot and see how it changed. Use **CTRL + C** to shut down the bot. Then run the command from earlier.
```shell
deno run --allow-net --allow-read mod.ts
deno run --allow-net --allow-read --no-check --config tsconfig.json mod.ts
```
To access this easily, most likely all you need to do is press the **UP ARROW** key. Feel free to copy paste this if it doesn't work.

View File

@@ -2,16 +2,15 @@ name: Discordeno
description: >-
Discord Deno TypeScript API library wrapper(Officially vetted library by
Discord Team) https://discordeno.netlify.app
version: 9.0.4
stable: true
entry: mod.ts
repository: 'https://github.com/Skillz4Killz/Discordeno'
files:
- ./src/**/*
- LICENSE
- mod.ts
- README.md
- tsconfig.json
- ./deps.ts
- mod.ts
checkAll: false
unlisted: false

View File

@@ -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) {

View File

@@ -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"
@@ -93,6 +93,11 @@ export let cacheHandlers = {
return cache[table].has(key);
},
/** Get the number of key-value pairs */
size: async (table: TableName) => {
return cache[table].size;
},
// Done differently to have overloads
/** Add a key value pair to the cache */
set,

View File

@@ -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) {

View File

@@ -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";

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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";
@@ -26,8 +26,8 @@ export async function handleInternalReady(
// Triggered on each shard
eventHandlers.shardReady?.(shardID);
if (payload.shard && shardID === payload.shard[1] - 1) {
// Wait 10 seconds to allow all guild create events to be processed
await delay(10000);
// Wait for 5 seconds to allow all guild create events to be processed
await delay(5000);
cache.isReady = true;
eventHandlers.ready?.();
}

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -1,8 +1,9 @@
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,9 +13,10 @@ 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 { RawOverwrite } from "../types/guild.ts";
import { MessageCreateOptions } from "../types/message.ts";
import { Permissions } from "../types/permission.ts";
import { endpoints } from "../utils/constants.ts";
import { botHasChannelPermissions } from "../utils/permissions.ts";
/** Checks if a channel overwrite for a user id or a role id has permission in this channel */
@@ -41,16 +43,22 @@ export async function getMessage(
channelID: string,
id: string,
) {
const hasViewChannelPerm = await botHasChannelPermissions(
channelID,
[Permissions.VIEW_CHANNEL],
);
if (
!botHasChannelPermissions(channelID, [Permissions.VIEW_CHANNEL])
!hasViewChannelPerm
) {
throw new Error(Errors.MISSING_VIEW_CHANNEL);
}
const hasReadMessageHistoryPerm = await botHasChannelPermissions(
channelID,
[Permissions.READ_MESSAGE_HISTORY],
);
if (
!botHasChannelPermissions(
channelID,
[Permissions.READ_MESSAGE_HISTORY],
)
!hasReadMessageHistoryPerm
) {
throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY);
}
@@ -70,16 +78,22 @@ export async function getMessages(
| GetMessagesAround
| GetMessages,
) {
const hasViewChannelPerm = await botHasChannelPermissions(
channelID,
[Permissions.VIEW_CHANNEL],
);
if (
!botHasChannelPermissions(channelID, [Permissions.VIEW_CHANNEL])
!hasViewChannelPerm
) {
throw new Error(Errors.MISSING_VIEW_CHANNEL);
}
const hasReadMessageHistoryPerm = await botHasChannelPermissions(
channelID,
[Permissions.READ_MESSAGE_HISTORY],
);
if (
!botHasChannelPermissions(
channelID,
[Permissions.READ_MESSAGE_HISTORY],
)
!hasReadMessageHistoryPerm
) {
throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY);
}
@@ -107,24 +121,34 @@ export async function sendMessage(
content: string | MessageContent,
) {
if (typeof content === "string") content = { content };
const hasSendMessagesPerm = await botHasChannelPermissions(
channelID,
[Permissions.SEND_MESSAGES],
);
if (
!botHasChannelPermissions(channelID, [Permissions.SEND_MESSAGES])
!hasSendMessagesPerm
) {
throw new Error(Errors.MISSING_SEND_MESSAGES);
}
const hasSendTtsMessagesPerm = await botHasChannelPermissions(
channelID,
[Permissions.SEND_TTS_MESSAGES],
);
if (
content.tts &&
!botHasChannelPermissions(
channelID,
[Permissions.SEND_TTS_MESSAGES],
)
!hasSendTtsMessagesPerm
) {
throw new Error(Errors.MISSING_SEND_TTS_MESSAGE);
}
const hasEmbedLinksPerm = await botHasChannelPermissions(
channelID,
[Permissions.EMBED_LINKS],
);
if (
content.embed &&
!botHasChannelPermissions(channelID, [Permissions.EMBED_LINKS])
!hasEmbedLinksPerm
) {
throw new Error(Errors.MISSING_EMBED_LINKS);
}
@@ -158,13 +182,41 @@ export async function sendMessage(
content.mentions.roles = content.mentions.roles.slice(0, 100);
}
}
if (content.mentions.repliedUser) {
if (
!(await botHasChannelPermissions(
channelID,
[Permissions.READ_MESSAGE_HISTORY],
))
) {
throw new Error(Errors.MISSING_SEND_MESSAGES);
}
}
}
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),
{
...content,
allowed_mentions: content.mentions,
allowed_mentions: content.mentions
? {
...content.mentions,
replied_user: content.mentions.repliedUser !== false,
}
: undefined,
message_reference: {
message_id: content.replyMessageID,
},
},
);
@@ -172,13 +224,17 @@ export async function sendMessage(
}
/** Delete messages from the channel. 2-100. Requires the MANAGE_MESSAGES permission */
export function deleteMessages(
export async function deleteMessages(
channelID: string,
ids: string[],
reason?: string,
) {
const hasManageMessages = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_MESSAGES],
);
if (
!botHasChannelPermissions(channelID, [Permissions.MANAGE_MESSAGES])
!hasManageMessages
) {
throw new Error(Errors.MISSING_MANAGE_MESSAGES);
}
@@ -199,9 +255,13 @@ export function deleteMessages(
}
/** Gets the invites for this channel. Requires MANAGE_CHANNEL */
export function getChannelInvites(channelID: string) {
export async function getChannelInvites(channelID: string) {
const hasManagaChannels = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_CHANNELS],
);
if (
!botHasChannelPermissions(channelID, [Permissions.MANAGE_CHANNELS])
!hasManagaChannels
) {
throw new Error(Errors.MISSING_MANAGE_CHANNELS);
}
@@ -209,12 +269,16 @@ export function getChannelInvites(channelID: string) {
}
/** Creates a new invite for this channel. Requires CREATE_INSTANT_INVITE */
export function createInvite(channelID: string, options: CreateInviteOptions) {
export async function createInvite(
channelID: string,
options: CreateInviteOptions,
) {
const hasCreateInstantInvitePerm = await botHasChannelPermissions(
channelID,
[Permissions.CREATE_INSTANT_INVITE],
);
if (
!botHasChannelPermissions(
channelID,
[Permissions.CREATE_INSTANT_INVITE],
)
!hasCreateInstantInvitePerm
) {
throw new Error(Errors.MISSING_CREATE_INSTANT_INVITE);
}
@@ -222,9 +286,13 @@ export function createInvite(channelID: string, options: CreateInviteOptions) {
}
/** Gets the webhooks for this channel. Requires MANAGE_WEBHOOKS */
export function getChannelWebhooks(channelID: string) {
export async function getChannelWebhooks(channelID: string) {
const hasManageWebhooksPerm = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_WEBHOOKS],
);
if (
!botHasChannelPermissions(channelID, [Permissions.MANAGE_WEBHOOKS])
!hasManageWebhooksPerm
) {
throw new Error(Errors.MISSING_MANAGE_WEBHOOKS);
}
@@ -277,12 +345,17 @@ function processEditChannelQueue() {
}
}
export function editChannel(
export async function editChannel(
channelID: string,
options: ChannelEditOptions,
reason?: string,
) {
const hasManageChannelsPerm = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_CHANNELS],
);
if (
!botHasChannelPermissions(channelID, [Permissions.MANAGE_CHANNELS])
!hasManageChannelsPerm
) {
throw new Error(Errors.MISSING_MANAGE_CHANNELS);
}
@@ -336,7 +409,10 @@ export function editChannel(
return RequestManager.patch(
endpoints.GUILD_CHANNEL(channelID),
payload,
{
...payload,
reason,
},
);
}
@@ -345,8 +421,12 @@ export async function followChannel(
sourceChannelID: string,
targetChannelID: string,
) {
const hasManageWebhooksPerm = await botHasChannelPermissions(
targetChannelID,
[Permissions.MANAGE_WEBHOOKS],
);
if (
!botHasChannelPermissions(targetChannelID, [Permissions.MANAGE_WEBHOOKS])
!hasManageWebhooksPerm
) {
throw new Error(Errors.MISSING_MANAGE_CHANNELS);
}
@@ -360,3 +440,30 @@ export async function followChannel(
return data.webhook_id;
}
/**
* Checks whether a channel is synchronized with its parent/category channel or not.
* @param channelID The ID of the channel to test for synchronization
* @return Returns `true` if the channel is synchronized, otherwise `false`. Returns `false` if the channel is not cached.
*/
export async function isChannelSynced(channelID: string) {
const channel = await cacheHandlers.get("channels", channelID);
if (!channel?.parentID) return false;
const parentChannel = await cacheHandlers.get("channels", channel.parentID);
if (!parentChannel) return false;
return channel.permission_overwrites?.every((overwrite) => {
const permission = parentChannel.permission_overwrites?.find((ow) =>
ow.id === overwrite.id
);
if (!permission) return false;
if (
overwrite.allow !== permission.allow || overwrite.deny !== permission.deny
) {
return false;
}
return true;
});
}

View File

@@ -1,38 +1,45 @@
import { endpoints } from "../constants/discord.ts";
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 { Template } from "../structures/template.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,
CreateEmojisOptions,
CreateGuildFromTemplate,
CreateGuildPayload,
CreateGuildTemplate,
CreateRoleOptions,
CreateServerOptions,
EditEmojisOptions,
EditGuildTemplate,
EditIntegrationOptions,
FetchMembersOptions,
GetAuditLogsOptions,
GuildEditOptions,
GuildTemplate,
PositionSwap,
PruneOptions,
PrunePayload,
UpdateGuildPayload,
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 { endpoints } from "../utils/constants.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. */
@@ -97,7 +104,11 @@ export async function createGuildChannel(
name: string,
options?: ChannelCreateOptions,
) {
if (!botHasPermission(guild.id, [Permissions.MANAGE_CHANNELS])) {
const hasPerm = await botHasPermission(
guild.id,
[Permissions.MANAGE_CHANNELS],
);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_CHANNELS);
}
@@ -126,12 +137,16 @@ export async function createGuildChannel(
}
/** Delete a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */
export function deleteChannel(
export async function deleteChannel(
guildID: string,
channelID: string,
reason?: string,
) {
if (!botHasPermission(guildID, [Permissions.MANAGE_CHANNELS])) {
const hasPerm = await botHasPermission(
guildID,
[Permissions.MANAGE_CHANNELS],
);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_CHANNELS);
}
@@ -186,16 +201,20 @@ export function swapChannels(
*
* ⚠️ **ADVANCED USE ONLY: Your members will be cached in your guild most likely. Only use this when you are absolutely sure the member is not cached.**
*/
export async function getMember(guildID: string, id: string) {
export async function getMember(
guildID: string,
id: string,
options?: { force?: boolean },
) {
const guild = await cacheHandlers.get("guilds", guildID);
if (!guild) return;
if (!guild && !options?.force) return;
const data = await RequestManager.get(
endpoints.GUILD_MEMBER(guildID, id),
) as MemberCreatePayload;
const member = await structures.createMember(data, guild.id);
guild.members.set(id, member);
const member = await structures.createMember(data, guildID);
guild?.members.set(id, member);
return member;
}
@@ -223,9 +242,8 @@ export async function createEmoji(
image: string,
options: CreateEmojisOptions,
) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_EMOJIS])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_EMOJIS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_EMOJIS);
}
@@ -241,16 +259,16 @@ export async function createEmoji(
}
/** Modify the given emoji. Requires the MANAGE_EMOJIS permission. */
export function editEmoji(
export async function editEmoji(
guildID: string,
id: string,
options: EditEmojisOptions,
) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_EMOJIS])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_EMOJIS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_EMOJIS);
}
return RequestManager.patch(endpoints.GUILD_EMOJI(guildID, id), {
name: options.name,
roles: options.roles,
@@ -258,12 +276,16 @@ export function editEmoji(
}
/** Delete the given emoji. Requires the MANAGE_EMOJIS permission. Returns 204 No Content on success. */
export function deleteEmoji(guildID: string, id: string, reason?: string) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_EMOJIS])
) {
export async function deleteEmoji(
guildID: string,
id: string,
reason?: string,
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_EMOJIS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_EMOJIS);
}
return RequestManager.delete(
endpoints.GUILD_EMOJI(guildID, id),
{ reason },
@@ -281,11 +303,11 @@ export async function createGuildRole(
options: CreateRoleOptions,
reason?: string,
) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_ROLES])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_ROLES);
}
const result = await RequestManager.post(
endpoints.GUILD_ROLES(guildID),
{
@@ -307,26 +329,31 @@ export async function createGuildRole(
}
/** Edit a guild role. Requires the MANAGE_ROLES permission. */
export function editRole(
export async function editRole(
guildID: string,
id: string,
options: CreateRoleOptions,
) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_ROLES])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
if (!hasPerm) {
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. */
export function deleteRole(guildID: string, id: string) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_ROLES])
) {
export async function deleteRole(guildID: string, id: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_ROLES);
}
return RequestManager.delete(endpoints.GUILD_ROLE(guildID, id));
}
@@ -334,22 +361,22 @@ export function deleteRole(guildID: string, id: string) {
*
* ⚠️ **If you need this, you are probably doing something wrong. This is not intended for use. Your roles will be cached in your guild.**
*/
export function getRoles(guildID: string) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_ROLES])
) {
export async function getRoles(guildID: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_ROLES);
}
return RequestManager.get(endpoints.GUILD_ROLES(guildID));
}
/** Modify the positions of a set of role objects for the guild. Requires the MANAGE_ROLES permission. */
export function swapRoles(guildID: string, rolePositons: PositionSwap) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_ROLES])
) {
export async function swapRoles(guildID: string, rolePositons: PositionSwap) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_ROLES);
}
return RequestManager.patch(endpoints.GUILD_ROLES(guildID), rolePositons);
}
@@ -358,9 +385,9 @@ export async function getPruneCount(guildID: string, options: PruneOptions) {
if (options.days < 1) {
throw new Error(Errors.PRUNE_MIN_DAYS);
}
if (
!botHasPermission(guildID, [Permissions.KICK_MEMBERS])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.KICK_MEMBERS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_KICK_MEMBERS);
}
@@ -373,13 +400,13 @@ export async function getPruneCount(guildID: string, options: PruneOptions) {
}
/** Begin pruning all members in the given time period */
export function pruneMembers(guildID: string, options: PruneOptions) {
export async function pruneMembers(guildID: string, options: PruneOptions) {
if (options.days < 1) {
throw new Error(Errors.PRUNE_MIN_DAYS);
}
if (
!botHasPermission(guildID, [Permissions.KICK_MEMBERS])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.KICK_MEMBERS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_KICK_MEMBERS);
}
@@ -400,8 +427,12 @@ export function fetchMembers(guild: Guild, options?: FetchMembersOptions) {
}
/** Returns the audit logs for the guild. Requires VIEW AUDIT LOGS permission */
export function getAuditLogs(guildID: string, options: GetAuditLogsOptions) {
if (!botHasPermission(guildID, [Permissions.VIEW_AUDIT_LOG])) {
export async function getAuditLogs(
guildID: string,
options: GetAuditLogsOptions,
) {
const hasPerm = await botHasPermission(guildID, [Permissions.VIEW_AUDIT_LOG]);
if (!hasPerm) {
throw new Error(Errors.MISSING_VIEW_AUDIT_LOG);
}
@@ -414,26 +445,26 @@ export function getAuditLogs(guildID: string, options: GetAuditLogsOptions) {
}
/** Returns the guild embed object. Requires the MANAGE_GUILD permission. */
export function getEmbed(guildID: string) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_GUILD])
) {
export async function getEmbed(guildID: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_GUILD);
}
return RequestManager.get(endpoints.GUILD_EMBED(guildID));
}
/** Modify a guild embed object for the guild. Requires the MANAGE_GUILD permission. */
export function editEmbed(
export async function editEmbed(
guildID: string,
enabled: boolean,
channelID?: string | null,
) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_GUILD])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_GUILD);
}
return RequestManager.patch(
endpoints.GUILD_EMBED(guildID),
{ enabled, channel_id: channelID },
@@ -446,26 +477,26 @@ export function getVanityURL(guildID: string) {
}
/** Returns a list of integrations for the guild. Requires the MANAGE_GUILD permission. */
export function getIntegrations(guildID: string) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_GUILD])
) {
export async function getIntegrations(guildID: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_GUILD);
}
return RequestManager.get(endpoints.GUILD_INTEGRATIONS(guildID));
}
/** Modify the behavior and settings of an integration object for the guild. Requires the MANAGE_GUILD permission. */
export function editIntegration(
export async function editIntegration(
guildID: string,
id: string,
options: EditIntegrationOptions,
) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_GUILD])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_GUILD);
}
return RequestManager.patch(
endpoints.GUILD_INTEGRATION(guildID, id),
options,
@@ -473,30 +504,29 @@ export function editIntegration(
}
/** Delete the attached integration object for the guild with this id. Requires MANAGE_GUILD permission. */
export function deleteIntegration(guildID: string, id: string) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_GUILD])
) {
export async function deleteIntegration(guildID: string, id: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_GUILD);
}
return RequestManager.delete(endpoints.GUILD_INTEGRATION(guildID, id));
}
/** Sync an integration. Requires the MANAGE_GUILD permission. */
export function syncIntegration(guildID: string, id: string) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_GUILD])
) {
export async function syncIntegration(guildID: string, id: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_GUILD);
}
return RequestManager.post(endpoints.GUILD_INTEGRATION_SYNC(guildID, id));
}
/** Returns a list of ban objects for the users banned from this guild. Requires the BAN_MEMBERS permission. */
export async function getBans(guildID: string) {
if (
!botHasPermission(guildID, [Permissions.BAN_MEMBERS])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.BAN_MEMBERS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_BAN_MEMBERS);
}
@@ -510,10 +540,9 @@ export async function getBans(guildID: string) {
}
/** Returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. */
export function getBan(guildID: string, memberID: string) {
if (
!botHasPermission(guildID, [Permissions.BAN_MEMBERS])
) {
export async function getBan(guildID: string, memberID: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.BAN_MEMBERS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_BAN_MEMBERS);
}
@@ -523,10 +552,9 @@ export function getBan(guildID: string, memberID: string) {
}
/** Ban a user from the guild and optionally delete previous messages sent by the user. Requires the BAN_MEMBERS permission. */
export function ban(guildID: string, id: string, options: BanOptions) {
if (
!botHasPermission(guildID, [Permissions.BAN_MEMBERS])
) {
export async function ban(guildID: string, id: string, options: BanOptions) {
const hasPerm = await botHasPermission(guildID, [Permissions.BAN_MEMBERS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_BAN_MEMBERS);
}
@@ -537,10 +565,9 @@ export function ban(guildID: string, id: string, options: BanOptions) {
}
/** Remove the ban for a user. REquires BAN_MEMBERS permission */
export function unban(guildID: string, id: string) {
if (
!botHasPermission(guildID, [Permissions.BAN_MEMBERS])
) {
export async function unban(guildID: string, id: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.BAN_MEMBERS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_BAN_MEMBERS);
}
return RequestManager.delete(endpoints.GUILD_BAN(guildID, id));
@@ -548,9 +575,8 @@ export function unban(guildID: string, id: string) {
/** Modify a guilds settings. Requires the MANAGE_GUILD permission. */
export async function editGuild(guildID: string, options: GuildEditOptions) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_GUILD])
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_GUILD);
}
@@ -570,12 +596,12 @@ export async function editGuild(guildID: string, options: GuildEditOptions) {
}
/** Get all the invites for this guild. Requires MANAGE_GUILD permission */
export function getInvites(guildID: string) {
if (
!botHasPermission(guildID, [Permissions.MANAGE_GUILD])
) {
export async function getInvites(guildID: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_GUILD);
}
return RequestManager.get(endpoints.GUILD_INVITES(guildID));
}
@@ -590,8 +616,12 @@ export function getVoiceRegions(guildID: string) {
}
/** Returns a list of guild webhooks objects. Requires the MANAGE_WEBHOOKs permission. */
export function getWebhooks(guildID: string) {
if (!botHasPermission(guildID, [Permissions.MANAGE_WEBHOOKS])) {
export async function getWebhooks(guildID: string) {
const hasPerm = await botHasPermission(
guildID,
[Permissions.MANAGE_WEBHOOKS],
);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_WEBHOOKS);
}
@@ -602,3 +632,158 @@ export function getWebhooks(guildID: string) {
export function getUser(userID: string) {
return RequestManager.get(endpoints.USER(userID)) as Promise<UserPayload>;
}
/**
* ⚠️ **If you need this, you are probably doing something wrong. Always use cache.guilds.get()
*
* Advanced Devs:
* This function fetches a guild's data. This is not the same data as a GUILD_CREATE.
* So it does not cache the guild, you must do it manually.
* */
export function getGuild(guildID: string, counts = true) {
return RequestManager.get(
endpoints.GUILD(guildID),
{ with_counts: counts },
) as Promise<UpdateGuildPayload>;
}
/** Returns the guild template if it exists */
export function getGuildTemplate(
guildID: string,
templateCode: string,
) {
return RequestManager.get(
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
) as Promise<Template>;
}
/**
* Create a new guild based on a template
* NOTE: This endpoint can be used only by bots in less than 10 guilds.
*/
export async function createGuildFromTemplate(
templateCode: string,
data: CreateGuildFromTemplate,
) {
if (await cacheHandlers.size("guilds") >= 10) {
throw new Error(
"This function can only be used by bots in less than 10 guilds.",
);
}
if (data.icon) {
data.icon = await urlToBase64(data.icon);
}
const guild = await RequestManager.post(
endpoints.GUILD_TEMPLATE(templateCode),
data,
) as Promise<CreateGuildPayload>;
return guild;
}
/**
* Returns an array of templates.
* Requires the `MANAGE_GUILD` permission.
*/
export async function getGuildTemplates(guildID: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
const templates = await RequestManager.get(
endpoints.GUILD_TEMPLATES(guildID),
) as GuildTemplate[];
return templates.map((template) => structures.createTemplate(template));
}
/**
* Deletes a template from a guild.
* Requires the `MANAGE_GUILD` permission.
*/
export async function deleteGuildTemplate(
guildID: string,
templateCode: string,
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
const deletedTemplate = await RequestManager.delete(
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
) as GuildTemplate;
return structures.createTemplate(deletedTemplate);
}
/**
* Creates a template for the guild.
* Requires the `MANAGE_GUILD` permission.
* @param name name of the template (1-100 characters)
* @param description description for the template (0-120 characters
*/
export async function createGuildTemplate(
guildID: string,
data: CreateGuildTemplate,
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
if (data.name.length < 1 || data.name.length > 100) {
throw new Error("The name can only be in between 1-100 characters.");
}
if (
data.description?.length &&
data.description.length > 120
) {
throw new Error("The description can only be in between 0-120 characters.");
}
const template = await RequestManager.post(
endpoints.GUILD_TEMPLATES(guildID),
data,
) as GuildTemplate;
return structures.createTemplate(template);
}
/**
* Syncs the template to the guild's current state.
* Requires the `MANAGE_GUILD` permission.
*/
export async function syncGuildTemplate(guildID: string, templateCode: string) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
const template = await RequestManager.put(
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
) as GuildTemplate;
return structures.createTemplate(template);
}
/**
* Edit a template's metadata.
* Requires the `MANAGE_GUILD` permission.
*/
export async function editGuildTemplate(
guildID: string,
templateCode: string,
data: EditGuildTemplate,
) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_GUILD]);
if (!hasPerm) throw new Error(Errors.MISSING_MANAGE_GUILD);
if (data.name?.length && (data.name.length < 1 || data.name.length > 100)) {
throw new Error("The name can only be in between 1-100 characters.");
}
if (
data.description?.length &&
data.description.length > 120
) {
throw new Error("The description can only be in between 0-120 characters.");
}
const template = await RequestManager.patch(
`${endpoints.GUILD_TEMPLATES(guildID)}/${templateCode}`,
data,
) as GuildTemplate;
return structures.createTemplate(template);
}

View File

@@ -1,23 +1,21 @@
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 { endpoints } from "../utils/constants.ts";
import {
botHasPermission,
higherRolePosition,
highestRole,
} from "../utils/permissions.ts";
import { urlToBase64 } from "../utils/utils.ts";
import { sendMessage } from "./channel.ts";
/** The users custom avatar or the default avatar if you don't have a member object. */
@@ -56,14 +54,19 @@ export async function addRole(
reason?: string,
) {
const botsHighestRole = await highestRole(guildID, botID);
if (
botsHighestRole &&
!higherRolePosition(guildID, botsHighestRole.id, roleID)
) {
throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW);
if (botsHighestRole) {
const hasHigherRolePosition = await higherRolePosition(
guildID,
botsHighestRole.id,
roleID,
);
if (!hasHigherRolePosition) {
throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW);
}
}
if (!botHasPermission(guildID, [Permissions.MANAGE_ROLES])) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_ROLES);
}
@@ -81,16 +84,23 @@ export async function removeRole(
reason?: string,
) {
const botsHighestRole = await highestRole(guildID, botID);
if (
botsHighestRole &&
!higherRolePosition(guildID, botsHighestRole.id, roleID)
) {
throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW);
if (botsHighestRole) {
const hasHigherRolePosition = await higherRolePosition(
guildID,
botsHighestRole.id,
roleID,
);
if (!hasHigherRolePosition) {
throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW);
}
}
if (!botHasPermission(guildID, [Permissions.MANAGE_ROLES])) {
const hasPerm = await botHasPermission(guildID, [Permissions.MANAGE_ROLES]);
if (!hasPerm) {
throw new Error(Errors.MISSING_MANAGE_ROLES);
}
return RequestManager.delete(
endpoints.GUILD_MEMBER_ROLE(guildID, memberID, roleID),
{ reason },
@@ -132,9 +142,11 @@ export async function kick(guildID: string, memberID: string, reason?: string) {
throw new Error(Errors.BOTS_HIGHEST_ROLE_TOO_LOW);
}
if (!botHasPermission(guildID, [Permissions.KICK_MEMBERS])) {
const hasPerm = await botHasPermission(guildID, [Permissions.KICK_MEMBERS]);
if (!hasPerm) {
throw new Error(Errors.MISSING_KICK_MEMBERS);
}
return RequestManager.delete(
endpoints.GUILD_MEMBER(guildID, memberID),
{ reason },
@@ -142,7 +154,7 @@ export async function kick(guildID: string, memberID: string, reason?: string) {
}
/** Edit the member */
export function editMember(
export async function editMember(
guildID: string,
memberID: string,
options: EditMemberOptions,
@@ -151,30 +163,47 @@ export function editMember(
if (options.nick.length > 32) {
throw new Error(Errors.NICKNAMES_MAX_LENGTH);
}
if (!botHasPermission(guildID, [Permissions.MANAGE_NICKNAMES])) {
const hasManageNickPerm = await botHasPermission(
guildID,
[Permissions.MANAGE_NICKNAMES],
);
if (!hasManageNickPerm) {
throw new Error(Errors.MISSING_MANAGE_NICKNAMES);
}
}
const hasManageRolesPerm = await botHasPermission(
guildID,
[Permissions.MANAGE_ROLES],
);
if (
options.roles &&
!botHasPermission(guildID, [Permissions.MANAGE_ROLES])
!hasManageRolesPerm
) {
throw new Error(Errors.MISSING_MANAGE_ROLES);
}
if (options.mute) {
const hasMuteMembersPerm = await botHasPermission(
guildID,
[Permissions.MUTE_MEMBERS],
);
// TODO: This should check if the member is in a voice channel
if (
!botHasPermission(guildID, [Permissions.MUTE_MEMBERS])
!hasMuteMembersPerm
) {
throw new Error(Errors.MISSING_MUTE_MEMBERS);
}
}
const hasDeafenMembersPerm = await botHasPermission(
guildID,
[Permissions.DEAFEN_MEMBERS],
);
if (
options.deaf &&
!botHasPermission(guildID, [Permissions.DEAFEN_MEMBERS])
!hasDeafenMembersPerm
) {
throw new Error(Errors.MISSING_DEAFEN_MEMBERS);
}
@@ -186,3 +215,48 @@ export function editMember(
options,
);
}
/**
* Move a member from a voice channel to another.
* @param guildID the id of the guild which the channel exists in
* @param memberID the id of the member to move.
* @param channelID id of channel to move user to (if they are connected to voice)
*/
export function moveMember(
guildID: string,
memberID: string,
channelID: string,
) {
return editMember(guildID, memberID, { channel_id: channelID });
}
/** Modifies the bot's username or avatar.
* NOTE: username: if changed may cause the bot's discriminator to be randomized.
*/
export function editBotProfile(username?: string, avatarURL?: string) {
// Nothing was edited
if (!username && !avatarURL) return;
// Check username requirements if username was provided
if (username) {
if (username.length > 32) {
throw new Error(Errors.USERNAME_MAX_LENGTH);
}
if (username.length < 2) {
throw new Error(Errors.USERNAME_MIN_LENGTH);
}
if (["@", "#", ":", "```"].some((char) => username.includes(char))) {
throw new Error(Errors.USERNAME_INVALID_CHARACTER);
}
if (["discordtag", "everyone", "here"].includes(username)) {
throw new Error(Errors.USERNAME_INVALID_USERNAME);
}
}
RequestManager.patch(
endpoints.USER_BOT,
{
username: username?.trim(),
avatar: avatarURL ? urlToBase64(avatarURL) : undefined,
},
);
}

View File

@@ -1,15 +1,15 @@
import { delay } from "../../deps.ts";
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 { endpoints } from "../utils/constants.ts";
import { botHasChannelPermissions } from "../utils/permissions.ts";
/** Delete a message with the channel id and message id only. */
@@ -38,11 +38,12 @@ export async function deleteMessage(
) {
if (message.author.id !== botID) {
// This needs to check the channels permission not the guild permission
const hasManageMessages = await botHasChannelPermissions(
message.channelID,
[Permissions.MANAGE_MESSAGES],
);
if (
!botHasChannelPermissions(
message.channelID,
[Permissions.MANAGE_MESSAGES],
)
!hasManageMessages
) {
throw new Error(Errors.MISSING_MANAGE_MESSAGES);
}
@@ -57,9 +58,13 @@ export async function deleteMessage(
}
/** Pin a message in a channel. Requires MANAGE_MESSAGES. Max pins allowed in a channel = 50. */
export function pin(channelID: string, messageID: string) {
export async function pin(channelID: string, messageID: string) {
const hasManageMessagesPerm = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_MESSAGES],
);
if (
!botHasChannelPermissions(channelID, [Permissions.MANAGE_MESSAGES])
!hasManageMessagesPerm
) {
throw new Error(Errors.MISSING_MANAGE_MESSAGES);
}
@@ -67,9 +72,13 @@ export function pin(channelID: string, messageID: string) {
}
/** Unpin a message in a channel. Requires MANAGE_MESSAGES. */
export function unpin(channelID: string, messageID: string) {
export async function unpin(channelID: string, messageID: string) {
const hasManageMessagesPerm = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_MESSAGES],
);
if (
!botHasChannelPermissions(channelID, [Permissions.MANAGE_MESSAGES])
!hasManageMessagesPerm
) {
throw new Error(Errors.MISSING_MANAGE_MESSAGES);
}
@@ -79,17 +88,25 @@ export function unpin(channelID: string, messageID: string) {
}
/** Create a reaction for the message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. Requires READ_MESSAGE_HISTORY and ADD_REACTIONS */
export function addReaction(
export async function addReaction(
channelID: string,
messageID: string,
reaction: string,
) {
if (!botHasChannelPermissions(channelID, [Permissions.ADD_REACTIONS])) {
const hasAddReactionsPerm = await botHasChannelPermissions(
channelID,
[Permissions.ADD_REACTIONS],
);
if (!hasAddReactionsPerm) {
throw new Error(Errors.MISSING_ADD_REACTIONS);
}
const hasReadMessageHistoryPerm = await botHasChannelPermissions(
channelID,
[Permissions.READ_MESSAGE_HISTORY],
);
if (
!botHasChannelPermissions(channelID, [Permissions.READ_MESSAGE_HISTORY])
!hasReadMessageHistoryPerm
) {
throw new Error(Errors.MISSING_READ_MESSAGE_HISTORY);
}
@@ -143,13 +160,17 @@ export function removeReaction(
}
/** Removes a reaction from the specified user on this message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. */
export function removeUserReaction(
export async function removeUserReaction(
channelID: string,
messageID: string,
reaction: string,
userID: string,
) {
if (!botHasChannelPermissions(channelID, [Permissions.MANAGE_MESSAGES])) {
const hasManageMessagesPerm = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_MESSAGES],
);
if (!hasManageMessagesPerm) {
throw new Error(Errors.MISSING_MANAGE_MESSAGES);
}
@@ -164,9 +185,13 @@ export function removeUserReaction(
}
/** Removes all reactions for all emojis on this message. */
export function removeAllReactions(channelID: string, messageID: string) {
export async function removeAllReactions(channelID: string, messageID: string) {
const hasManageMessagesPerm = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_MESSAGES],
);
if (
!botHasChannelPermissions(channelID, [Permissions.MANAGE_MESSAGES])
!hasManageMessagesPerm
) {
throw new Error(Errors.MISSING_MANAGE_MESSAGES);
}
@@ -176,13 +201,17 @@ export function removeAllReactions(channelID: string, messageID: string) {
}
/** Removes all reactions for a single emoji on this message. Reaction takes the form of **name:id** for custom guild emoji, or Unicode characters. */
export function removeReactionEmoji(
export async function removeReactionEmoji(
channelID: string,
messageID: string,
reaction: string,
) {
const hasManageMessagesPerm = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_MESSAGES],
);
if (
!botHasChannelPermissions(channelID, [Permissions.MANAGE_MESSAGES])
!hasManageMessagesPerm
) {
throw new Error(Errors.MISSING_MANAGE_MESSAGES);
}
@@ -216,18 +245,23 @@ export async function editMessage(
if (typeof content === "string") content = { content };
const hasSendMessagesPerm = await botHasChannelPermissions(
message.channelID,
[Permissions.SEND_MESSAGES],
);
if (
!botHasChannelPermissions(message.channelID, [Permissions.SEND_MESSAGES])
!hasSendMessagesPerm
) {
throw new Error(Errors.MISSING_SEND_MESSAGES);
}
const hasSendTtsMessagesPerm = await botHasChannelPermissions(
message.channelID,
[Permissions.SEND_TTS_MESSAGES],
);
if (
content.tts &&
!botHasChannelPermissions(
message.channelID,
[Permissions.SEND_TTS_MESSAGES],
)
!hasSendTtsMessagesPerm
) {
throw new Error(Errors.MISSING_SEND_TTS_MESSAGE);
}

203
src/handlers/mod.ts Normal file
View File

@@ -0,0 +1,203 @@
import {
channelOverwriteHasPermission,
createInvite,
deleteMessages,
editChannel,
followChannel,
getChannelInvites,
getChannelWebhooks,
getMessage,
getMessages,
getPins,
isChannelSynced,
sendMessage,
} from "./channel.ts";
import {
ban,
categoryChildrenIDs,
createEmoji,
createGuildChannel,
createGuildFromTemplate,
createGuildRole,
createGuildTemplate,
createServer,
deleteChannel,
deleteEmoji,
deleteGuildTemplate,
deleteIntegration,
deleteRole,
deleteServer,
editEmbed,
editEmoji,
editGuild,
editGuildTemplate,
editIntegration,
editRole,
emojiURL,
fetchMembers,
getAuditLogs,
getBan,
getBans,
getChannel,
getChannels,
getEmbed,
getGuild,
getGuildTemplate,
getGuildTemplates,
getIntegrations,
getInvites,
getMember,
getMembersByQuery,
getPruneCount,
getRoles,
getUser,
getVanityURL,
getVoiceRegions,
getWebhooks,
guildBannerURL,
guildIconURL,
guildSplashURL,
leaveGuild,
pruneMembers,
swapChannels,
swapRoles,
syncGuildTemplate,
syncIntegration,
unban,
} from "./guild.ts";
import {
addRole,
avatarURL,
editBotProfile,
editMember,
kick,
moveMember,
rawAvatarURL,
removeRole,
sendDirectMessage,
} from "./member.ts";
import {
addReaction,
addReactions,
deleteMessage,
deleteMessageByID,
editMessage,
getReactions,
pin,
publishMessage,
removeAllReactions,
removeReaction,
removeReactionEmoji,
removeUserReaction,
unpin,
} from "./message.ts";
import { createWebhook, executeWebhook, getWebhook } from "./webhook.ts";
export let handlers = {
// Channel handler
channelOverwriteHasPermission,
createInvite,
deleteMessages,
editChannel,
followChannel,
getChannelInvites,
getChannelWebhooks,
getMessage,
getMessages,
getPins,
isChannelSynced,
sendMessage,
// Guild handler
ban,
categoryChildrenIDs,
createEmoji,
createGuildChannel,
createGuildFromTemplate,
createGuildRole,
createGuildTemplate,
createServer,
deleteChannel,
deleteEmoji,
deleteGuildTemplate,
deleteIntegration,
deleteRole,
deleteServer,
editEmbed,
editEmoji,
editGuild,
editGuildTemplate,
editIntegration,
editRole,
emojiURL,
fetchMembers,
getAuditLogs,
getBan,
getBans,
getChannel,
getChannels,
getEmbed,
getGuild,
getGuildTemplate,
getGuildTemplates,
getIntegrations,
getInvites,
getMember,
getMembersByQuery,
getPruneCount,
getRoles,
getUser,
getVanityURL,
getVoiceRegions,
getWebhooks,
guildBannerURL,
guildIconURL,
guildSplashURL,
leaveGuild,
pruneMembers,
swapChannels,
swapRoles,
syncGuildTemplate,
syncIntegration,
unban,
// Member handler
addRole,
avatarURL,
editBotProfile,
editMember,
kick,
moveMember,
rawAvatarURL,
removeRole,
sendDirectMessage,
// Message handler
addReaction,
addReactions,
deleteMessage,
deleteMessageByID,
editMessage,
getReactions,
pin,
publishMessage,
removeAllReactions,
removeReaction,
removeReactionEmoji,
removeUserReaction,
unpin,
// Webhook handler
createWebhook,
executeWebhook,
getWebhook,
};
export type Handlers = typeof handlers;
export function updateHandlers(newHandlers: Partial<Handlers>) {
handlers = {
...handlers,
...newHandlers,
};
}

View File

@@ -1,14 +1,14 @@
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,
} from "../types/webhook.ts";
import { endpoints } from "../utils/constants.ts";
import { botHasChannelPermissions } from "../utils/permissions.ts";
import { urlToBase64 } from "../utils/utils.ts";
@@ -20,11 +20,12 @@ export async function createWebhook(
channelID: string,
options: WebhookCreateOptions,
) {
const hasManageWebhooksPerm = await botHasChannelPermissions(
channelID,
[Permissions.MANAGE_WEBHOOKS],
);
if (
!botHasChannelPermissions(
channelID,
[Permissions.MANAGE_WEBHOOKS],
)
!hasManageWebhooksPerm
) {
throw new Error(Errors.MISSING_MANAGE_WEBHOOKS);
}

View File

@@ -7,16 +7,15 @@ 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<number, BasicShard>();

View File

@@ -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 { endpoints } from "../utils/constants.ts";
import { RequestManager } from "./requestManager.ts";
import { spawnShards } from "./shardingManager.ts";

View File

@@ -1,8 +1,8 @@
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 { baseEndpoints } from "../utils/constants.ts";
import { authorization, eventHandlers } from "./client.ts";
const pathQueues: { [key: string]: QueuedRequest[] } = {};
@@ -64,56 +64,60 @@ async function cleanupQueues() {
}
async function processQueue() {
if (
(Object.keys(pathQueues).length) && !globallyRateLimited
) {
await Promise.allSettled(
Object.values(pathQueues).map(async (pathQueue) => {
const request = pathQueue.shift();
if (!request) return;
// Putting this code inside a function like this allows us to use tail recursion like a while loop without hitting the max stack error.
async function avoidMaxStackError() {
if (
(Object.keys(pathQueues).length) && !globallyRateLimited
) {
await Promise.allSettled(
Object.values(pathQueues).map(async (pathQueue) => {
const request = pathQueue.shift();
if (!request) return;
const rateLimitedURLResetIn = await checkRatelimits(request.url);
const rateLimitedURLResetIn = await checkRatelimits(request.url);
if (request.bucketID) {
const rateLimitResetIn = await checkRatelimits(request.bucketID);
if (rateLimitResetIn) {
// This request is still rate limited readd to queue
addToQueue(request);
} else if (rateLimitedURLResetIn) {
// This URL is rate limited readd to queue
addToQueue(request);
if (request.bucketID) {
const rateLimitResetIn = await checkRatelimits(request.bucketID);
if (rateLimitResetIn) {
// This request is still rate limited readd to queue
addToQueue(request);
} else if (rateLimitedURLResetIn) {
// This URL is rate limited readd to queue
addToQueue(request);
} else {
// This request is not rate limited so it should be run
const result = await request.callback();
if (result && result.rateLimited) {
addToQueue(
{ ...request, bucketID: result.bucketID || request.bucketID },
);
}
}
} else {
// This request is not rate limited so it should be run
const result = await request.callback();
if (result && result.rateLimited) {
addToQueue(
{ ...request, bucketID: result.bucketID || request.bucketID },
);
if (rateLimitedURLResetIn) {
// This URL is rate limited readd to queue
addToQueue(request);
} else {
// This request has no bucket id so it should be processed
const result = await request.callback();
if (request && result && result.rateLimited) {
addToQueue(
{ ...request, bucketID: result.bucketID || request.bucketID },
);
}
}
}
} else {
if (rateLimitedURLResetIn) {
// This URL is rate limited readd to queue
addToQueue(request);
} else {
// This request has no bucket id so it should be processed
const result = await request.callback();
if (request && result && result.rateLimited) {
addToQueue(
{ ...request, bucketID: result.bucketID || request.bucketID },
);
}
}
}
}),
);
}),
);
}
if (Object.keys(pathQueues).length) {
avoidMaxStackError();
cleanupQueues();
} else queueInProcess = false;
}
if (Object.keys(pathQueues).length) {
await delay(1000);
processQueue();
cleanupQueues();
} else queueInProcess = false;
return avoidMaxStackError();
}
processRateLimitedPaths();
@@ -196,7 +200,7 @@ async function runMethod(
},
);
const errorStack = new Error("Location In Your Files:");
const errorStack = new Error("Location:");
Error.captureStackTrace(errorStack);
return new Promise((resolve, reject) => {
@@ -286,6 +290,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,27 +317,40 @@ 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:
console.error(
"The request was improperly formatted, or the server couldn't understand it.",
);
throw errorStack;
case HttpResponseCode.Unauthorized:
console.error("The Authorization header was missing or invalid.");
throw errorStack;
case HttpResponseCode.Forbidden:
console.error(
"The Authorization token you passed did not have permission to the resource.",
);
throw errorStack;
case HttpResponseCode.NotFound:
console.error("The resource at the location specified doesn't exist.");
throw errorStack;
case HttpResponseCode.MethodNotAllowed:
throw new Error(Errors.REQUEST_CLIENT_ERROR);
console.error(
"The HTTP method used is not valid for the location specified.",
);
throw errorStack;
case HttpResponseCode.GatewayUnavailable:
throw new Error(Errors.REQUEST_SERVER_ERROR);
console.error(
"There was not a gateway available to process your request. Wait a bit and retry.",
);
throw errorStack;
// left are all unknown
default:
console.error(Errors.REQUEST_UNKNOWN_ERROR);
throw errorStack;
}
// left are all unknown
throw new Error(Errors.REQUEST_UNKNOWN_ERROR);
}
function processHeaders(url: string, headers: Headers) {
@@ -350,7 +384,7 @@ function processHeaders(url: string, headers: Headers) {
// If there is no remaining global limit, we save it in cache
if (global) {
const reset = Date.now() + Number(retryAfter);
const reset = Date.now() + (Number(retryAfter) * 1000);
eventHandlers.debug?.(
{ type: "globallyRateLimited", data: { url, reset } },
);

View File

@@ -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;
@@ -111,6 +115,7 @@ export async function handleDiscordPayload(
shardID: number,
) {
eventHandlers.raw?.(data);
await eventHandlers.dispatchRequirements?.(data, shardID);
switch (data.op) {
case GatewayOpcode.HeartbeatACK:

View File

@@ -1,6 +1,6 @@
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 { Unpromise } from "../types/misc.ts";
import { calculatePermissions } from "../utils/permissions.ts";
export async function createChannel(

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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 {
@@ -11,11 +11,14 @@ export async function createMessage(data: MessageCreateOptions) {
webhook_id: webhookID,
message_reference: messageReference,
edited_timestamp: editedTimestamp,
referenced_message: referencedMessageID,
...rest
} = data;
const message = {
...rest,
/** The message id of the original message if this message was sent as a reply. If null, the original message was deleted. */
referencedMessageID,
channelID,
guildID: guildID || "",
mentions: data.mentions.map((m) => m.id),

View File

@@ -3,6 +3,7 @@ import { createGuild } from "./guild.ts";
import { createMember } from "./member.ts";
import { createMessage } from "./message.ts";
import { createRole } from "./role.ts";
import { createTemplate } from "./template.ts";
/** This is the placeholder where the structure creation functions are kept. */
export let structures = {
@@ -11,6 +12,7 @@ export let structures = {
createMember,
createMessage,
createRole,
createTemplate,
};
export type Structures = typeof structures;

View File

@@ -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 {

View File

@@ -0,0 +1,31 @@
import { GuildTemplate } from "../types/guild.ts";
export function createTemplate(
data: GuildTemplate,
) {
const {
usage_count: usageCount,
creator_id: creatorID,
created_at: createdAt,
updated_at: updatedAt,
source_guild_id: sourceGuildID,
serialized_source_guild: serializedSourceGuild,
is_dirty: isDirty,
...rest
} = data;
const template = {
...rest,
usageCount,
creatorID,
createdAt,
updatedAt,
sourceGuildID,
serializedSourceGuild,
isDirty,
};
return template;
}
export interface Template extends ReturnType<typeof createTemplate> {}

View File

@@ -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,
}

View File

@@ -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 */
@@ -101,6 +101,8 @@ export interface MessageContent {
roles?: string[];
/** Array of user_ids to mention (Max size of 100) */
users?: string[];
/** Should the message author from the original message be mention. By default this is true. */
repliedUser?: boolean;
};
/** The message contents, up to 2000 characters */
content?: string;
@@ -114,6 +116,8 @@ export interface MessageContent {
embed?: Embed;
/** JSON encoded body of any additional request fields. */
payload_json?: string;
/** If you want to send a reply message, provide the original message id here */
replyMessageID?: string;
}
export interface GetMessages {

View File

@@ -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 */
@@ -201,11 +201,6 @@ export interface Properties {
$device: string;
}
export interface Timestamps {
start?: number;
end?: number;
}
export interface Emoji {
name: string;
id?: string;

View File

@@ -31,4 +31,10 @@ 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",
USERNAME_MAX_LENGTH = "USERNAME_MAX_LENGTH",
USERNAME_MIN_LENGTH = "USERNAME_MIN_LENGTH",
USERNAME_INVALID_CHARACTER = "USERNAME_INVALID_CHARACTER",
USERNAME_INVALID_USERNAME = "USERNAME_INVALID_USERNAME",
}

View File

@@ -1,10 +1,11 @@
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 { Guild } from "../structures/guild.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 */
@@ -606,3 +607,50 @@ export interface CreateServerOptions {
/** the id of the channel where guild notices such as welcome messages and boost events are posted */
system_channel_id?: string;
}
// https://discord.com/developers/docs/resources/template#template-object
export interface GuildTemplate {
/** the template code (unique ID) */
code: string;
/** template name */
name: string;
/** the description for the template */
description: string | null;
/** number of times this template has been used */
usage_count: number;
/** the ID of the user who created the template */
creator_id: string;
/** the user who created the template */
user: UserPayload;
/** when this template was created */
created_at: string;
/** when this template was last synced to the source guild */
updated_at: string;
/** the ID of the guild this template is based on */
source_guild_id: string;
/** the guild snapshot this template contains */
serialized_source_guild: Guild;
/** whether the template has unsynced changes */
is_dirty: boolean | null;
}
export interface CreateGuildFromTemplate {
/** name of the guild (2-100 characters) */
name: string;
/** base64 128x128 image for the guild icon */
icon?: string;
}
export interface CreateGuildTemplate {
/** name of the template (1-100 characters) */
name: string;
/** description for the template (0-120 characters) */
description?: string;
}
export interface EditGuildTemplate {
/** name of the template (1-100 characters) */
name?: string;
/** description for the template (0-120 characters) */
description?: string | null;
}

View File

@@ -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. */

View File

@@ -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;
@@ -154,6 +154,11 @@ export enum MessageTypes {
USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2,
USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3,
CHANNEL_FOLLOW_ADD,
GUILD_DISCOVERY_DISQUALIFIED = 14,
GUILD_DISCOVERY_REQUALIFIED,
GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING,
GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING,
REPLY = 19,
}
export enum ActivityTypes {
@@ -275,6 +280,8 @@ export interface MessageCreateOptions {
message_reference?: Reference;
/** The message flags combined like permission bits describe extra features of the message */
flags?: 1 | 2 | 4 | 8 | 16;
/** The message id of the original message if this message was sent as a reply. If null, the original message was deleted. */
referenced_message?: MessageCreateOptions | null;
}
export interface BaseMessageDeletePayload {

View File

@@ -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,
@@ -84,6 +84,7 @@ export interface EventHandlers {
channelUpdate?: (channel: Channel, cachedChannel: Channel) => unknown;
channelDelete?: (channel: Channel) => unknown;
debug?: (args: DebugArg) => unknown;
dispatchRequirements?: (data: DiscordPayload, shardID: number) => unknown;
guildBanAdd?: (guild: Guild, user: Member | UserPayload) => unknown;
guildBanRemove?: (guild: Guild, user: Member | UserPayload) => unknown;
guildCreate?: (guild: Guild) => unknown;

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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<string, Function>(),
fetchAllMembersProcessingRequests: new Collection(),
};

View File

@@ -1,4 +1,4 @@
import type { ImageFormats, ImageSize } from "../types/cdn.ts";
import { ImageFormats, ImageSize } from "../types/cdn.ts";
export const formatImageURL = (
url: string,

View File

@@ -89,6 +89,9 @@ export const endpoints = {
`${baseEndpoints.CDN_URL}/splashes/${id}/${icon}`,
GUILD_VANITY_URL: (id: string) => `${GUILDS_BASE(id)}/vanity-url`,
GUILD_WEBHOOKS: (id: string) => `${GUILDS_BASE(id)}/webhooks`,
GUILD_TEMPLATE: (code: string) =>
`${baseEndpoints.BASE_URL}/guilds/templates/${code}`,
GUILD_TEMPLATES: (id: string) => `${GUILDS_BASE(id)}/templates`,
WEBHOOK: (id: string, token: string) =>
`${baseEndpoints.BASE_URL}/webhooks/${id}/${token}`,
@@ -96,6 +99,7 @@ export const endpoints = {
// User endpoints
USER: (id: string) => `${baseEndpoints.BASE_URL}/users/${id}`,
USER_BOT: `${baseEndpoints.BASE_URL}/users/@me`,
USER_AVATAR: (id: string, icon: string) =>
`${baseEndpoints.CDN_URL}/avatars/${id}/${icon}`,
USER_DEFAULT_AVATAR: (icon: number) =>

View File

@@ -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 { RawOverwrite } 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;
@@ -56,8 +58,11 @@ export async function botHasPermission(
const member = guild.members.get(botID);
if (!member) return false;
const permissionBits = member.roles
// The everyone role is not in member.roles
const permissionBits = [...member.roles, guild.id]
.map((id) => guild.roles.get(id)!)
// Remove any edge case undefined
.filter((r) => r)
.reduce((bits, data) => {
bits |= BigInt(data.permissions);
@@ -84,101 +89,98 @@ 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: RawOverwrite | undefined;
let everyoneOverwrite: RawOverwrite | undefined;
let rolesOverwrites: RawOverwrite[] = [];
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.permission_overwrites || []) {
// 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<Permissions>();
// 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) => {
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(memberOverwrite.deny) & 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)) {
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) {
// This perm is allowed so we save it
if (BigInt(overwrite.allow) & BigInt(perm)) {
allowedPermissions.add(perm);
break;
}
// If this role denies it we need to save and check if another role allows it, allows > deny
if (BigInt(overwrite.deny) & 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(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) {
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(everyoneOverwrite.deny) & BigInt(perm)) return false;
// This perm is allowed so we save it
if (BigInt(everyoneOverwrite.allow) & BigInt(perm)) {
allowedPermissions.add(perm);
}
}
}
return botHasPermission(guild.id, permissions);
// 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
const hasPerms = await botHasPermission(guild.id, permissions);
return hasPerms;
}
/** 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 +188,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;

View File

@@ -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));

253
tests/mod.test.ts Normal file
View File

@@ -0,0 +1,253 @@
import { assert, assertEquals, delay } from "../deps.ts";
import {
botID,
cache,
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() {
await editChannel(data.channelID, {
name: "edited-channel",
overwrites: [
{
id: data.roleID,
type: OverwriteType.ROLE,
allow: ["VIEW_CHANNEL", "SEND_MESSAGES"],
deny: ["USE_EXTERNAL_EMOJIS"],
},
],
});
// Wait 5 seconds for it to update
await delay(5000);
const editedChannel = await getChannel(data.channelID);
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.permission_overwrites) throw "Channel overwrites not found.";
const hasPerm = channelOverwriteHasPermission(
data.guildID,
data.roleID,
channel.permission_overwrites,
[Permissions.VIEW_CHANNEL, Permissions.SEND_MESSAGES],
);
const missingPerm = channelOverwriteHasPermission(
data.guildID,
data.roleID,
channel.permission_overwrites,
[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();
},
...testOptions,
});