Merge pull request #159 from Skillz4Killz/rename-permission_overwrites

Rename permission overwrites
This commit is contained in:
Skillz4Killz
2020-10-29 16:27:45 -04:00
committed by GitHub
49 changed files with 713 additions and 311 deletions

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"

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,15 +2,19 @@
> Discord API library wrapper in Deno
[![Discord](https://img.shields.io/discord/223909216866402304?color=7289da&logo=discord&logoColor=white)](https://discord.gg/J4NqJ72)
[![Discord](https://img.shields.io/discord/223909216866402304?color=7289da&logo=discord&logoColor=dark)](https://discord.gg/J4NqJ72)
![Testing/Linting](https://github.com/Skillz4Killz/Discordeno/workflows/Testing/Linting/badge.svg)
[![nest.land](https://nest.land/badge-large.svg)](https://nest.land/package/Discordeno)
[![nest badge](https://nest.land/badge.svg)](https://nest.land/package/Discordeno)
[Website](https://discordeno.netlify.app)
[WIP] ![Test](https://github.com/Skillz4Killz/Discordeno/workflows/Test/badge.svg)
## Beginner Developers
## Why Discordeno?
Don't worry a lot of developers start out coding their first projects as a Discord bot(I did 😉) and it is not so easy. With Discordeno, I tried to build it in a way that solved all the headaches I had when first starting out coding bots. If you are a beginner developer, please use a boilerplate: The official one is at: [GitHub](https://github.com/Skillz4Killz/Discordeno-bot-template) but there will be more listed on the website. It is a beautiful website indeed! Check it out!
### Beginner Developers
Don't worry a lot of developers start out coding their first projects as a Discord bot (I did 😉) and it is not so easy to do so. Discordeno is built considering all the issues wit pre-existing libraries, such as discord.js, and issues that I had when I first started out coding bots.
If you are a beginner developer, please use this official boilerplate: [GitHub](https://github.com/Skillz4Killz/Discordeno-bot-template) but there will be more listed on the website. It is a beautiful website indeed! Check it out!
**Modular commands, arguments, events, inhibitors, monitors, tasks.**
@@ -41,15 +45,10 @@ Don't worry a lot of developers start out coding their first projects as a Disco
- Uses i18next, one of the best localization tools available.
- Supports nested folders to keep cleaner translation files
**Hot Reloadable**
- **Hot Reloadable**: Easily update your code without having to restart the bot everytime.
- **Step By Step Guide**: There is a step by step walkthrough to learn how to create Discord bots with Discordeno on our website!
- Easily update your code without having to restart the bot everytime.
**Step By Step Guide**
- There is a step by step walkthrough to learn how to create Discord bots with Discordeno on our website!
## Advanced Developers
### Advanced Developers
The instructions below are meant for advanced developers!
@@ -64,7 +63,7 @@ StartBot({
intents: [Intents.GUILD_MESSAGES, Intents.GUILDS],
eventHandlers: {
ready: () => {
console.log(`Logged!`);
console.log('Successfully connected to gateway');
},
messageCreate: (message) => {
if (message.content === "!ping") {
@@ -77,8 +76,13 @@ StartBot({
Alternatively, you can use boilerplate template repositories that were created by wonderful developers. Review the list on the website, and add any of yours if you make your own.
![image](https://i.imgur.com/z1BfUnt.png)
## Documentation
#### Dark Mode
- [API Documentation](https://doc.deno.land/https/deno.land/x/discordeno/mod.ts)
- [Guide](https://discordeno.netlify.com)
- [Support server](https://discord.gg/J4NqJ72)
- [Contributing Guide](https://github.com/Skillz4Killz/Discordeno/blob/master/.github/CONTRIBUTING.md)
![image](https://i.imgur.com/Vr2Bebr.png)
## License
MIT © Skillz4Killz

View File

@@ -7,4 +7,9 @@ export {
isWebSocketPongEvent,
} from "https://deno.land/std@0.67.0/ws/mod.ts";
export type { WebSocket } from "https://deno.land/std@0.67.0/ws/mod.ts";
export {
assert,
assertArrayIncludes,
assertEquals,
} from "https://deno.land/std@0.75.0/testing/asserts.ts";
export { decompress_with as inflate } from "https://unpkg.com/@evan/wasm@0.0.11/target/zlib/deno.js";

View File

@@ -54,43 +54,44 @@ const nekosEndpoints = [
{ name: "gecg", path: "/img/gecg", nsfw: false },
{ name: "avatar", path: "/img/avatar", nsfw: false },
{ name: "waifu", path: "/img/waifu", nsfw: false },
{ name: "randomHentaiGif", path: "/img/Random_hentai_gif", nsfw: true },
{ name: "pussy", path: "/img/pussy", nsfw: true },
{ name: "nekoGif", path: "/img/nsfw_neko_gif", nsfw: true },
{ name: "neko", path: "/img/lewd", nsfw: true },
{ name: "lesbian", path: "/img/les", nsfw: true },
{ name: "kuni", path: "/img/kuni", nsfw: true },
{ name: "cumsluts", path: "/img/cum", nsfw: true },
{ name: "classic", path: "/img/classic", nsfw: true },
{ name: "boobs", path: "/img/boobs", nsfw: true },
{ name: "bJ", path: "/img/bj", nsfw: true },
{ name: "anal", path: "/img/anal", nsfw: true },
{ name: "avatar", path: "/img/nsfw_avatar", nsfw: true },
{ name: "yuri", path: "/img/yuri", nsfw: true },
{ name: "trap", path: "/img/trap", nsfw: true },
{ name: "tits", path: "/img/tits", nsfw: true },
{ name: "girlSoloGif", path: "/img/solog", nsfw: true },
{ name: "girlSolo", path: "/img/solo", nsfw: true },
{ name: "pussyWankGif", path: "/img/pwankg", nsfw: true },
{ name: "pussyArt", path: "/img/pussy_jpg", nsfw: true },
{ name: "kemonomimi", path: "/img/lewdkemo", nsfw: true },
{ name: "kitsune", path: "/img/lewdk", nsfw: true },
{ name: "keta", path: "/img/keta", nsfw: true },
{ name: "holo", path: "/img/hololewd", nsfw: true },
{ name: "holoEro", path: "/img/holoero", nsfw: true },
{ name: "hentai", path: "/img/hentai", nsfw: true },
{ name: "futanari", path: "/img/futanari", nsfw: true },
{ name: "femdom", path: "/img/femdom", nsfw: true },
{ name: "feetGif", path: "/img/feetg", nsfw: true },
{ name: "eroFeet", path: "/img/erofeet", nsfw: true },
{ name: "feet", path: "/img/feet", nsfw: true },
{ name: "ero", path: "/img/ero", nsfw: true },
{ name: "eroKitsune", path: "/img/erok", nsfw: true },
{ name: "eroKemonomimi", path: "/img/erokemo", nsfw: true },
{ name: "eroNeko", path: "/img/eron", nsfw: true },
{ name: "eroYuri", path: "/img/eroyuri", nsfw: true },
{ name: "cumArts", path: "/img/cum_jpg", nsfw: true },
{ name: "blowJob", path: "/img/blowjob", nsfw: true },
// The follow name and paths have been hidden for this guide as they are NSFW.
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
{ name: "nsfw_endpoint", path: "/img/nsfw_example", nsfw: true },
];
nekosEndpoints.forEach((endpoint) => {

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

@@ -75,7 +75,8 @@ Alternatively, you can use boilerplate template repositories that were created b
| DenoBot | NTM Nathan#0001 | [GitHub](https://github.com/ntm-development/DenoBot), [Support Server](https://discord.com/invite/G2rb53z) | Another boilerplate example of the first one, with more commands and improvements. |
| Discordeno Helper | Suyashtnt | [Github](https://github.com/Suyashtnt/discordeno-helper-template/) | A reimplementation of DenoBot using the [discordeno-helper](https://github.com/Suyashtnt/discordeno-helper) framework
Open Sourced Bots:
**Open Sourced Bots:**
| Bot Name | Developer | Links |
| ----------------- | ---------- | ---------------------------------------------------------- |
| discordeno-mattis | Mattis6666 | [Github](https://github.com/Mattis6666/discordeno-mattis/) |

View File

@@ -19,6 +19,12 @@ Discordeno is a Third Party Deno Library for interacting with the Discord API.
- Latest and Greatest JavaScript
- Actively Maintained!
### User Reviews
If you wish to leave a review for other users, please send a PR adding your review to this section!
Using the Discord API with types but such a simple language like TypeScript is so easy now. Discordeno is A W E S O M E! -[LukasDoesDev](https://github.com/LukasDoesDev)
## Read me first...
Discordeno is cool right? You could make the next big bot! Who knows, but before we get right into developing our Bot. We want to get started with learning the basics...

View File

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

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"

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,
@@ -41,11 +41,6 @@ export async function handleInternalGuildMemberRemove(data: DiscordPayload) {
member || payload.user,
);
eventHandlers.guildMemberRemove?.(
guild,
member || payload.user,
);
guild.members.delete(payload.user.id);
}

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

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,10 @@
import { endpoints } from "../constants/discord.ts";
import { cacheHandlers } from "../controllers/cache.ts";
import { RequestManager } from "../module/requestManager.ts";
import { structures } from "../structures/mod.ts";
import type {
import {
ChannelEditOptions,
ChannelTypes,
CreateInviteOptions,
FollowedChannelPayload,
GetMessages,
@@ -12,16 +14,19 @@ import type {
MessageContent,
} from "../types/channel.ts";
import { Errors } from "../types/errors.ts";
import type { RawOverwrite } from "../types/guild.ts";
import type { MessageCreateOptions } from "../types/message.ts";
import { PermissionOverwrite } from "../types/guild.ts";
import { MessageCreateOptions } from "../types/message.ts";
import { Permissions } from "../types/permission.ts";
import { botHasChannelPermissions } from "../utils/permissions.ts";
import {
botHasChannelPermissions,
calculateBits,
} from "../utils/permissions.ts";
/** Checks if a channel overwrite for a user id or a role id has permission in this channel */
export function channelOverwriteHasPermission(
guildID: string,
id: string,
overwrites: RawOverwrite[],
overwrites: PermissionOverwrite[],
permissions: Permissions[],
) {
const overwrite = overwrites.find((perm) => perm.id === id) ||
@@ -29,8 +34,10 @@ export function channelOverwriteHasPermission(
return permissions.every((perm) => {
if (overwrite) {
if (BigInt(overwrite.deny) & BigInt(perm)) return false;
if (BigInt(overwrite.allow) & BigInt(perm)) return true;
const allowBits = calculateBits(overwrite.allow);
const denyBits = calculateBits(overwrite.deny);
if (BigInt(denyBits) & BigInt(perm)) return false;
if (BigInt(allowBits) & BigInt(perm)) return true;
}
return false;
});
@@ -160,6 +167,15 @@ export async function sendMessage(
}
}
const channel = await cacheHandlers.get("channels", channelID);
if (!channel) throw new Error(Errors.CHANNEL_NOT_FOUND);
if (
![ChannelTypes.DM, ChannelTypes.GUILD_NEWS, ChannelTypes.GUILD_TEXT]
.includes(channel.type)
) {
throw new Error(Errors.CHANNEL_NOT_TEXT_BASED);
}
const result = await RequestManager.post(
endpoints.CHANNEL_MESSAGES(channelID),
{

View File

@@ -3,13 +3,13 @@ import { cacheHandlers } from "../controllers/cache.ts";
import { identifyPayload } from "../module/client.ts";
import { RequestManager } from "../module/requestManager.ts";
import { requestAllMembers } from "../module/shardingManager.ts";
import type { Guild } from "../structures/guild.ts";
import type { Member } from "../structures/member.ts";
import { Guild } from "../structures/guild.ts";
import { Member } from "../structures/member.ts";
import { structures } from "../structures/mod.ts";
import type { ImageFormats, ImageSize } from "../types/cdn.ts";
import { ImageFormats, ImageSize } from "../types/cdn.ts";
import { ChannelCreatePayload, ChannelTypes } from "../types/channel.ts";
import { Errors } from "../types/errors.ts";
import type {
import {
BannedUser,
BanOptions,
ChannelCreateOptions,
@@ -26,13 +26,13 @@ import type {
PrunePayload,
UserPayload,
} from "../types/guild.ts";
import type { MemberCreatePayload } from "../types/member.ts";
import { MemberCreatePayload } from "../types/member.ts";
import { Intents } from "../types/options.ts";
import { Permissions } from "../types/permission.ts";
import type { RoleData } from "../types/role.ts";
import { RoleData } from "../types/role.ts";
import { formatImageURL } from "../utils/cdn.ts";
import { Collection } from "../utils/collection.ts";
import { botHasPermission } from "../utils/permissions.ts";
import { botHasPermission, calculateBits } from "../utils/permissions.ts";
import { urlToBase64 } from "../utils/utils.ts";
/** Create a new guild. Returns a guild object on success. Fires a Guild Create Gateway event. This endpoint can be used only by bots in less than 10 guilds. */
@@ -105,7 +105,7 @@ export async function createGuildChannel(
(await RequestManager.post(endpoints.GUILD_CHANNELS(guild.id), {
...options,
name,
permission_overwrites: options?.permission_overwrites?.map((perm) => ({
permission_overwrites: options?.permissionOverwrites?.map((perm) => ({
...perm,
allow: perm.allow.reduce(
@@ -317,7 +317,12 @@ export function editRole(
) {
throw new Error(Errors.MISSING_MANAGE_ROLES);
}
return RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), options);
return RequestManager.patch(endpoints.GUILD_ROLE(guildID, id), {
...options,
permissions: options.permissions
? calculateBits(options.permissions)
: undefined,
});
}
/** Delete a guild role. Requires the MANAGE_ROLES permission. */

View File

@@ -2,15 +2,12 @@ import { endpoints } from "../constants/discord.ts";
import { cacheHandlers } from "../controllers/cache.ts";
import { botID } from "../module/client.ts";
import { RequestManager } from "../module/requestManager.ts";
import type { Member } from "../structures/member.ts";
import { Member } from "../structures/member.ts";
import { structures } from "../structures/mod.ts";
import type { ImageFormats, ImageSize } from "../types/cdn.ts";
import type {
DMChannelCreatePayload,
MessageContent,
} from "../types/channel.ts";
import { ImageFormats, ImageSize } from "../types/cdn.ts";
import { DMChannelCreatePayload, MessageContent } from "../types/channel.ts";
import { Errors } from "../types/errors.ts";
import type { EditMemberOptions } from "../types/member.ts";
import { EditMemberOptions } from "../types/member.ts";
import { Permissions } from "../types/permission.ts";
import { formatImageURL } from "../utils/cdn.ts";
import {

View File

@@ -3,12 +3,12 @@ import { endpoints } from "../constants/discord.ts";
import { cacheHandlers } from "../controllers/cache.ts";
import { botID } from "../module/client.ts";
import { RequestManager } from "../module/requestManager.ts";
import type { Message } from "../structures/message.ts";
import { Message } from "../structures/message.ts";
import { structures } from "../structures/mod.ts";
import type { MessageContent } from "../types/channel.ts";
import { MessageContent } from "../types/channel.ts";
import { Errors } from "../types/errors.ts";
import type { UserPayload } from "../types/guild.ts";
import type { MessageCreateOptions } from "../types/message.ts";
import { UserPayload } from "../types/guild.ts";
import { MessageCreateOptions } from "../types/message.ts";
import { Permissions } from "../types/permission.ts";
import { botHasChannelPermissions } from "../utils/permissions.ts";

View File

@@ -2,9 +2,9 @@ import { endpoints } from "../constants/discord.ts";
import { RequestManager } from "../module/requestManager.ts";
import { structures } from "../structures/mod.ts";
import { Errors } from "../types/errors.ts";
import type { MessageCreateOptions } from "../types/message.ts";
import { MessageCreateOptions } from "../types/message.ts";
import { Permissions } from "../types/permission.ts";
import type {
import {
ExecuteWebhookOptions,
WebhookCreateOptions,
WebhookPayload,

View File

@@ -7,21 +7,22 @@ import {
isWebSocketPongEvent,
WebSocket,
} from "../../deps.ts";
import type {
import {
DiscordBotGatewayData,
DiscordHeartbeatPayload,
GatewayOpcode,
ReadyPayload,
} from "../types/discord.ts";
import { GatewayOpcode } from "../types/discord.ts";
import type { FetchMembersOptions } from "../types/guild.ts";
import type { BotStatusRequest } from "../utils/utils.ts";
import type { IdentifyPayload } from "./client.ts";
import { botGatewayData, eventHandlers } from "./client.ts";
import { FetchMembersOptions } from "../types/guild.ts";
import { BotStatusRequest } from "../utils/utils.ts";
import { botGatewayData, eventHandlers, IdentifyPayload } from "./client.ts";
import { handleDiscordPayload } from "./shardingManager.ts";
const basicShards = new Map<number, BasicShard>();
const heartbeating = new Set<number>();
const heartbeating = new Map<number, boolean>();
const utf8decoder = new TextDecoder();
const RequestMembersQueue: RequestMemberQueuedRequest[] = [];
let processQueue = false;
export interface BasicShard {
id: number;
@@ -32,9 +33,6 @@ export interface BasicShard {
needToResume: boolean;
}
const RequestMembersQueue: RequestMemberQueuedRequest[] = [];
let processQueue = false;
interface RequestMemberQueuedRequest {
guildID: string;
shardID: number;
@@ -52,7 +50,7 @@ export async function createBasicShard(
const basicShard: BasicShard = {
id: shardID,
socket: await connectWebSocket(`${data.url}?v=6&encoding=json`),
socket: await connectWebSocket(`${data.url}?v=8&encoding=json`),
resumeInterval: 0,
sessionID: oldShard?.sessionID || "",
previousSequenceNumber: oldShard?.previousSequenceNumber || 0,
@@ -123,9 +121,13 @@ export async function createBasicShard(
heartbeat(
basicShard,
(data.d as DiscordHeartbeatPayload).heartbeat_interval,
identifyPayload,
);
}
break;
case GatewayOpcode.HeartbeatACK:
heartbeating.set(shardID, true);
break;
case GatewayOpcode.Reconnect:
eventHandlers.debug?.(
{ type: "reconnect", data: { shardID: basicShard.id } },
@@ -200,17 +202,39 @@ function resume(shard: BasicShard, payload: IdentifyPayload) {
}));
}
// TODO: If a client does not receive a heartbeat ack between its attempts at sending heartbeats, it should immediately terminate the connection with a non-1000 close code, reconnect, and attempt to resume.
async function heartbeat(
shard: BasicShard,
interval: number,
payload: IdentifyPayload,
) {
// We lost socket connection between heartbeats, resume connection
if (shard.socket.isClosed) {
shard.needToResume = true;
resumeConnection(botGatewayData, payload, shard.id);
heartbeating.delete(shard.id);
return;
}
if (!heartbeating.has(shard.id)) heartbeating.add(shard.id);
if (heartbeating.has(shard.id)) {
const receivedACK = heartbeating.get(shard.id);
// If a ACK response was not received since last heartbeat, issue invalid session close
if (!receivedACK) {
eventHandlers.debug?.(
{
type: "heartbeatStopped",
data: {
interval,
previousSequenceNumber: shard.previousSequenceNumber,
shardID: shard.id,
},
},
);
return shard.socket.send(JSON.stringify({ op: 4009 }));
}
}
// Set it to false as we are issuing a new heartbeat
heartbeating.set(shard.id, false);
shard.socket.send(
JSON.stringify(
@@ -228,7 +252,7 @@ async function heartbeat(
},
);
await delay(interval);
heartbeat(shard, interval);
heartbeat(shard, interval, payload);
}
async function resumeConnection(

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

View File

@@ -2,7 +2,7 @@ import { delay } from "../../deps.ts";
import { baseEndpoints } from "../constants/discord.ts";
import { HttpResponseCode } from "../types/discord.ts";
import { Errors } from "../types/errors.ts";
import type { RequestMethods } from "../types/fetch.ts";
import { RequestMethods } from "../types/fetch.ts";
import { authorization, eventHandlers } from "./client.ts";
const pathQueues: { [key: string]: QueuedRequest[] } = {};
@@ -286,6 +286,23 @@ async function runMethod(
});
}
async function logErrors(response: Response, errorStack?: unknown) {
try {
const error = await response.json();
console.error(error);
eventHandlers.debug?.({ type: "error", data: { errorStack, error } });
} catch {
eventHandlers.debug?.(
{
type: "error",
data: { errorStack },
},
);
console.error(response);
}
}
function handleStatusCode(response: Response, errorStack?: unknown) {
const status = response.status;
@@ -296,13 +313,7 @@ function handleStatusCode(response: Response, errorStack?: unknown) {
return true;
}
eventHandlers.debug?.(
{
type: "error",
data: { errorStack },
},
);
console.error(response);
logErrors(response, errorStack);
switch (status) {
case HttpResponseCode.BadRequest:

View File

@@ -1,13 +1,17 @@
import type { WebSocket } from "../../deps.ts";
import { connectWebSocket, delay, isWebSocketCloseEvent } from "../../deps.ts";
import type {
import {
connectWebSocket,
delay,
isWebSocketCloseEvent,
WebSocket,
} from "../../deps.ts";
import {
DiscordBotGatewayData,
DiscordHeartbeatPayload,
GatewayOpcode,
ReadyPayload,
} from "../types/discord.ts";
import { GatewayOpcode } from "../types/discord.ts";
import type { FetchMembersOptions } from "../types/guild.ts";
import type { DebugArg } from "../types/options.ts";
import { FetchMembersOptions } from "../types/guild.ts";
import { DebugArg } from "../types/options.ts";
let shardSocket: WebSocket;

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;

View File

@@ -1,6 +1,7 @@
import { cacheHandlers } from "../controllers/cache.ts";
import type { ChannelCreatePayload } from "../types/channel.ts";
import type { Unpromise } from "../types/misc.ts";
import { ChannelCreatePayload } from "../types/channel.ts";
import { PermissionOverwrite } from "../types/guild.ts";
import { Unpromise } from "../types/misc.ts";
import { calculatePermissions } from "../utils/permissions.ts";
export async function createChannel(
@@ -14,13 +15,14 @@ export async function createChannel(
rate_limit_per_user: rateLimitPerUser,
parent_id: parentID,
last_pin_timestamp: lastPinTimestamp,
permission_overwrites,
...rest
} = data;
const channel = {
...rest,
/** The guild id of the channel if it is a guild channel. */
guildID: guildID || rawGuildID,
guildID: guildID || rawGuildID || "",
/** The id of the last message sent in this channel */
lastMessageID,
/** The amount of users allowed in this voice channel. */
@@ -32,13 +34,14 @@ export async function createChannel(
/** The last time when a message was pinned in this channel */
lastPinTimestamp,
/** The permission overwrites for this channel */
permissions: data.permission_overwrites
? data.permission_overwrites.map((perm) => ({
...perm,
allow: calculatePermissions(BigInt(perm.allow)),
deny: calculatePermissions(BigInt(perm.deny)),
}))
: [],
permissionOverwrites:
(data.permission_overwrites
? data.permission_overwrites.map((perm) => ({
...perm,
allow: calculatePermissions(BigInt(perm.allow)),
deny: calculatePermissions(BigInt(perm.deny)),
}))
: []) as PermissionOverwrite[],
/** Whether this channel is nsfw or not */
nsfw: data.nsfw || false,
/** The mention of the channel */

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 {

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

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

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 */
@@ -196,11 +196,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,6 @@ export enum Errors {
CHANNEL_NOT_IN_GUILD = "CHANNEL_NOT_IN_GUILD",
INVALID_WEBHOOK_NAME = "INVALID_WEBHOOK_NAME",
INVALID_WEBHOOK_OPTIONS = "INVALID_WEBHOOK_OPTIONS",
CHANNEL_NOT_FOUND = "CHANNEL_NOT_FOUND",
CHANNEL_NOT_TEXT_BASED = "CHANNEL_NOT_TEXT_BASED",
}

View File

@@ -1,10 +1,10 @@
import type { ChannelCreatePayload, ChannelTypes } from "./channel.ts";
import type { Emoji, StatusType } from "./discord.ts";
import type { MemberCreatePayload } from "./member.ts";
import type { Activity } from "./message.ts";
import type { Permission } from "./permission.ts";
import type { ClientStatusPayload } from "./presence.ts";
import type { RoleData } from "./role.ts";
import { ChannelCreatePayload, ChannelTypes } from "./channel.ts";
import { Emoji, StatusType } from "./discord.ts";
import { MemberCreatePayload } from "./member.ts";
import { Activity } from "./message.ts";
import { Permission } from "./permission.ts";
import { ClientStatusPayload } from "./presence.ts";
import { RoleData } from "./role.ts";
export interface GuildRolePayload {
/** The id of the guild */
@@ -473,6 +473,12 @@ export interface RawOverwrite {
deny: number;
}
export interface PermissionOverwrite
extends Omit<RawOverwrite, "allow" | "deny"> {
allow: Permission[];
deny: Permission[];
}
export interface ChannelCreateOptions {
/** The type of the channel */
type?: ChannelTypes;
@@ -487,7 +493,7 @@ export interface ChannelCreateOptions {
/** The sorting position of the channel */
position?: number;
/** The channel's permission overwrites */
permission_overwrites?: Overwrite[];
permissionOverwrites?: Overwrite[];
/** The id of the parent category for the channel */
parent_id?: string;
/** Whether the channel is nsfw */

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;

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,
@@ -64,6 +64,7 @@ export interface DebugArg {
| "requestManagerFetched"
| "requestMembersProcessing"
| "heartbeat"
| "heartbeatStopped"
| "createShard"
| "invalidSession"
| "reconnect"

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

@@ -1,9 +1,9 @@
import { cacheHandlers } from "../controllers/cache.ts";
import { botID } from "../module/client.ts";
import type { Guild } from "../structures/guild.ts";
import type { Role } from "../structures/role.ts";
import type { Permission } from "../types/permission.ts";
import { Permissions } from "../types/permission.ts";
import { Guild } from "../structures/guild.ts";
import { Role } from "../structures/role.ts";
import { PermissionOverwrite } from "../types/guild.ts";
import { Permission, Permissions } from "../types/permission.ts";
/** Checks if the member has this permission. If the member is an owner or has admin perms it will always be true. */
export async function memberIDHasPermission(
@@ -34,6 +34,8 @@ export function memberHasPermission(
const permissionBits = memberRoleIDs.map((id) =>
guild.roles.get(id)?.permissions
)
// Removes any edge case undefined
.filter((id) => id)
.reduce((bits, permissions) => {
bits |= BigInt(permissions);
return bits;
@@ -58,6 +60,8 @@ export async function botHasPermission(
const permissionBits = member.roles
.map((id) => guild.roles.get(id)!)
// Remove any edge case undefined
.filter((r) => r)
.reduce((bits, data) => {
bits |= BigInt(data.permissions);
@@ -84,101 +88,104 @@ export async function hasChannelPermissions(
permissions: Permissions[],
) {
const channel = await cacheHandlers.get("channels", channelID);
if (!channel?.guildID) return true;
if (!channel) return false;
if (!channel.guildID) return true;
const guild = await cacheHandlers.get("guilds", channel.guildID);
if (!guild) return false;
if (guild.ownerID === memberID) return true;
if (botHasPermission(guild.id, [Permissions.ADMINISTRATOR])) return true;
if (
await memberIDHasPermission(memberID, guild.id, ["ADMINISTRATOR"])
) {
return true;
}
const member = guild.members.get(memberID);
if (!member) return false;
const memberOverwrite = channel.permission_overwrites?.find((o) =>
o.id === memberID
);
let memberOverwrite: PermissionOverwrite | undefined;
let everyoneOverwrite: PermissionOverwrite | undefined;
let rolesOverwrites: PermissionOverwrite[] = [];
const rolesOverwrites = channel.permission_overwrites?.filter((o) =>
member.roles.includes(o.id)
);
const everyoneOverwrite = channel.permission_overwrites?.find((o) =>
o.id === guild.id
);
for (const overwrite of channel.permissionOverwrites || []) {
// If the overwrite on this channel is specific to this member
if (overwrite.id === memberID) memberOverwrite = overwrite;
// If it is the everyone role overwrite
if (overwrite.id === guild.id) everyoneOverwrite = overwrite;
// If it is one of the roles the member has
if (member.roles.includes(overwrite.id)) rolesOverwrites.push(overwrite);
}
const allowedPermissions = new Set<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) => {
const allowBits = calculateBits(memberOverwrite.allow);
const denyBits = calculateBits(memberOverwrite.deny);
for (const perm of permissions) {
// One of the necessary permissions is denied. Since this is main permission we can cancel if its denied.
if (BigInt(denyBits) & BigInt(perm)) return false;
// Already allowed perm
if (allowedPermissions.has(perm)) return;
if (allowedPermissions.has(perm)) continue;
// This perm is allowed so we save it
if (BigInt(memberOverwrite.allow) & BigInt(perm)) {
if (BigInt(allowBits) & BigInt(perm)) {
allowedPermissions.add(perm);
}
});
}
}
// Check the necessary permissions for roles
if (rolesOverwrites?.length) {
if (
rolesOverwrites.some((overwrite) =>
permissions.some((perm) =>
(BigInt(overwrite.deny) & BigInt(perm)) &&
// If another role allows these perms then they are not denied
!rolesOverwrites.some((o) => BigInt(o.allow) & BigInt(perm)) &&
// Make sure the memberOverwrite does not allow this perm
!(memberOverwrite && BigInt(memberOverwrite.allow) & BigInt(perm))
)
)
) {
return false;
}
for (const perm of permissions) {
// If this is already allowed, skip
if (allowedPermissions.has(perm)) continue;
permissions.forEach((perm) => {
for (const overwrite of rolesOverwrites) {
const allowBits = calculateBits(overwrite.allow);
// This perm is allowed so we save it
if (BigInt(allowBits) & BigInt(perm)) {
allowedPermissions.add(perm);
break;
}
const denyBits = calculateBits(overwrite.deny);
// If this role denies it we need to save and check if another role allows it, allows > deny
if (BigInt(denyBits) & BigInt(perm)) {
// This role denies his perm, but before denying we need to check all other roles if any allow as allow > deny
const isAllowed = rolesOverwrites.some((o) =>
BigInt(calculateBits(o.allow)) & BigInt(perm)
);
if (isAllowed) continue;
// This permission is in fact denied. Since Roles overrule everything below here we can cancel ou here
return false;
}
}
}
if (everyoneOverwrite) {
const allowBits = calculateBits(everyoneOverwrite.allow);
const denyBits = calculateBits(everyoneOverwrite.deny);
for (const perm of permissions) {
// Already allowed perm
if (allowedPermissions.has(perm)) return;
rolesOverwrites.forEach((overwrite) => {
// This perm is allowed so we save it
if (BigInt(overwrite.allow) & BigInt(perm)) {
allowedPermissions.add(perm);
}
});
});
}
// Check the necessary permissions for everyone
if (
everyoneOverwrite
) {
if (
permissions.some((perm) =>
BigInt(everyoneOverwrite.deny) & BigInt(perm) &&
!allowedPermissions.has(perm)
)
) {
return false;
}
// If all permissions are granted
if (
permissions.every((perm) =>
BigInt(everyoneOverwrite.allow) & BigInt(perm)
)
) {
return true;
if (allowedPermissions.has(perm)) continue;
// One of the necessary permissions is denied. Since everyone overwrite overrides role perms we can cancel here
if (BigInt(denyBits) & BigInt(perm)) return false;
// This perm is allowed so we save it
if (BigInt(allowBits) & BigInt(perm)) {
allowedPermissions.add(perm);
}
}
}
// Is there any remaining permission to check role perms or can we determine that permissions are allowed
if (permissions.every((perm) => allowedPermissions.has(perm))) return true;
// Some permission was not explicitly allowed so we default to checking role perms directly
return botHasPermission(guild.id, permissions);
}
/** This function converts a bitwise string to permission strings */
export function calculatePermissions(permissionBits: bigint) {
return Object.keys(Permissions).filter((perm) => {
if (typeof perm !== "number") return false;
@@ -186,6 +193,14 @@ export function calculatePermissions(permissionBits: bigint) {
}) as Permission[];
}
/** This function converts an array of permissions into the bitwise string. */
export function calculateBits(permissions: Permission[]) {
return permissions.reduce(
(bits, perm) => bits |= BigInt(Permissions[perm]),
BigInt(0),
).toString();
}
export async function highestRole(guildID: string, memberID: string) {
const guild = await cacheHandlers.get("guilds", guildID);
if (!guild) return;

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,
Channel,
createClient,
createGuildChannel,
createGuildRole,
createServer,
deleteChannel,
deleteRole,
deleteServer,
editRole,
getMessage,
Guild,
Intents,
OverwriteType,
Role,
sendMessage,
} from "../mod.ts";
import {
channelOverwriteHasPermission,
editChannel,
} from "../src/handlers/channel.ts";
import { getChannel } from "../src/handlers/guild.ts";
import { Permissions } from "../src/types/permission.ts";
const token = Deno.env.get("DISCORD_TOKEN");
if (!token) throw "Token is not provided";
createClient({
token,
intents: [Intents.GUILD_MESSAGES, Intents.GUILDS],
});
// Default options for all test cases
const testOptions = {
sanitizeOps: false,
sanitizeResources: false,
};
Deno.test({
name: "connect to the gateway",
fn: async () => {
// Delay the execution by 15 seconds (15000 ms)
await delay(15000);
// Check whether botID is nil or not
assert(botID);
},
...testOptions,
});
const data = {
guildID: "",
roleID: "",
channelID: "",
};
Deno.test({
name: "create a guild",
async fn() {
// Create a guild "Discordeno Test"
const createdGuild = (await createServer({
name: "Discordeno Test",
})) as Guild;
// Check whether createdGuild is nil or not
assert(createdGuild);
data.guildID = createdGuild.id;
},
...testOptions,
});
// Role
Deno.test({
name: "create a role in a guild",
async fn() {
// Create a role "Role 1" in the guild "Discordeno Test"
const createdRole = await createGuildRole(data.guildID, {
name: "Role 1",
});
// Check whether the created role is nil or not
assert(createdRole);
data.roleID = createdRole.id;
},
...testOptions,
});
Deno.test({
name: "edit a role in a guild",
async fn() {
// Edit a role "Role 1" in the guild "Discordeno Test"
const editedRole = (await editRole(data.guildID, data.roleID, {
name: "Edited Role",
color: 4320244,
hoist: false,
mentionable: false,
})) as Role;
// Assertions
assert(editedRole);
assertEquals(editedRole.name, "Edited Role");
assertEquals(editedRole.color, 4320244);
assertEquals(editedRole.hoist, false);
assertEquals(editedRole.mentionable, false);
data.roleID = editedRole.id;
},
...testOptions,
});
// Channel
Deno.test({
name: "create a channel in a guild",
async fn() {
const guild = cache.guilds.get(data.guildID);
if (!guild) throw "Guild not found";
const createdChannel = await createGuildChannel(guild, "test");
// Check whether the created channel is nil or not
assert(createdChannel);
data.channelID = createdChannel.id;
},
...testOptions,
});
Deno.test({
name: "get a channel in a guild",
async fn() {
const channel = await getChannel(data.channelID);
assertEquals(channel.id, data.channelID);
},
...testOptions,
});
Deno.test({
name: "edit a channel in a guild",
async fn() {
const channel = await editChannel(data.channelID, {
name: "edited channel",
overwrites: [
{
id: data.roleID,
type: OverwriteType.ROLE,
allow: ["VIEW_CHANNEL", "SEND_MESSAGES"],
deny: ["USE_EXTERNAL_EMOJIS"],
},
],
}) as Channel;
const editedChannel = await getChannel(data.channelID);
assert(channel);
assertEquals(editedChannel.name, "edited channel");
},
});
Deno.test({
name: "channel overwrite has permission",
async fn() {
const channel = cache.channels.get(data.channelID);
if (!channel) throw "Channel not found";
if (!channel.permissionOverwrites) throw "Channel overwrites not found.";
const hasPerm = channelOverwriteHasPermission(
data.guildID,
data.roleID,
channel.permissionOverwrites,
[Permissions.VIEW_CHANNEL, Permissions.SEND_MESSAGES],
);
const missingPerm = channelOverwriteHasPermission(
data.guildID,
data.roleID,
channel.permissionOverwrites,
[Permissions.USE_EXTERNAL_EMOJIS],
);
assertEquals(hasPerm, true);
assertEquals(missingPerm, false);
},
...testOptions,
});
// Message
let messageID: string;
Deno.test({
name: "create a message in a guild",
async fn() {
const createdMessage = await sendMessage(data.channelID, "test");
// Check whether the created message is nil or not
assert(createdMessage);
messageID = createdMessage.id;
},
});
Deno.test({
name: "get a message in a guild",
async fn() {
const message = await getMessage(data.channelID, messageID);
assertEquals(messageID, message.id);
},
});
// Clean up
Deno.test({
name: "delete a role from the guild",
async fn() {
await deleteRole(data.guildID, data.roleID);
data.roleID = "";
assertEquals(data.roleID, "");
},
});
Deno.test({
name: "delete a channel in the guild",
async fn() {
await deleteChannel(data.guildID, data.channelID);
},
...testOptions,
});
Deno.test({
name: "delete a guild",
async fn() {
await deleteServer(data.guildID);
data.guildID = "";
assertEquals(data.guildID, "");
},
...testOptions,
});
// This is meant to be the final test that forcefully crashes the bot
Deno.test({
name: "exit the process forcefully after all the tests are done",
async fn() {
Deno.exit(1);
},
...testOptions,
});