This commit is contained in:
Skillz
2020-11-17 23:46:13 -05:00
27 changed files with 970 additions and 280 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

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

View File

@@ -3,7 +3,7 @@
> Discord API library wrapper in Deno
[![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)
![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)
@@ -27,7 +27,7 @@ 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({
@@ -57,4 +57,4 @@ Alternatively, you can use boilerplate template repositories that were created b
## License
MIT © Skillz4Killz
[MIT © Skillz4Killz](https://github.com/Skillz4Killz/Discordeno/blob/master/LICENSE)

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

@@ -2,7 +2,6 @@ name: Discordeno
description: >-
Discord Deno TypeScript API library wrapper(Officially vetted library by
Discord Team) https://discordeno.netlify.app
version: 9.0.15
stable: true
entry: mod.ts
repository: 'https://github.com/Skillz4Killz/Discordeno'

View File

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

@@ -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,4 +1,3 @@
import { endpoints } from "../constants/discord.ts";
import { cacheHandlers } from "../controllers/cache.ts";
import { RequestManager } from "../module/requestManager.ts";
import { structures } from "../structures/mod.ts";
@@ -17,10 +16,8 @@ import { Errors } from "../types/errors.ts";
import { PermissionOverwrite } from "../types/guild.ts";
import { MessageCreateOptions } from "../types/message.ts";
import { Permissions } from "../types/permission.ts";
import {
botHasChannelPermissions,
calculateBits,
} from "../utils/permissions.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 */
export function channelOverwriteHasPermission(
@@ -48,16 +45,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);
}
@@ -77,16 +80,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);
}
@@ -114,24 +123,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);
}
@@ -165,6 +184,17 @@ 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);
@@ -180,7 +210,15 @@ export async function sendMessage(
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,
},
},
);
@@ -188,13 +226,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);
}
@@ -215,9 +257,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);
}
@@ -225,12 +271,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);
}
@@ -238,9 +288,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);
}
@@ -293,12 +347,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);
}
@@ -352,7 +411,10 @@ export function editChannel(
return RequestManager.patch(
endpoints.GUILD_CHANNEL(channelID),
payload,
{
...payload,
reason,
},
);
}
@@ -361,8 +423,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);
}
@@ -376,3 +442,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,4 +1,3 @@
import { endpoints } from "../constants/discord.ts";
import { cacheHandlers } from "../controllers/cache.ts";
import { identifyPayload } from "../module/client.ts";
import { RequestManager } from "../module/requestManager.ts";
@@ -6,6 +5,7 @@ import { requestAllMembers } from "../module/shardingManager.ts";
import { Guild } from "../structures/guild.ts";
import { Member } from "../structures/member.ts";
import { structures } from "../structures/mod.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";
@@ -14,16 +14,22 @@ import {
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 { MemberCreatePayload } from "../types/member.ts";
@@ -32,6 +38,7 @@ import { Permissions } from "../types/permission.ts";
import { RoleData } from "../types/role.ts";
import { formatImageURL } from "../utils/cdn.ts";
import { Collection } from "../utils/collection.ts";
import { endpoints } from "../utils/constants.ts";
import { botHasPermission, calculateBits } from "../utils/permissions.ts";
import { urlToBase64 } from "../utils/utils.ts";
@@ -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,16 +329,16 @@ 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,
permissions: options.permissions
@@ -326,12 +348,12 @@ export function editRole(
}
/** 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));
}
@@ -339,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);
}
@@ -363,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);
}
@@ -378,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);
}
@@ -405,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);
}
@@ -419,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 },
@@ -451,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,
@@ -478,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);
}
@@ -515,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);
}
@@ -528,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);
}
@@ -542,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));
@@ -553,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);
}
@@ -575,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));
}
@@ -595,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);
}
@@ -607,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,4 +1,3 @@
import { endpoints } from "../constants/discord.ts";
import { cacheHandlers } from "../controllers/cache.ts";
import { botID } from "../module/client.ts";
import { RequestManager } from "../module/requestManager.ts";
@@ -10,11 +9,13 @@ import { Errors } from "../types/errors.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. */
@@ -53,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);
}
@@ -78,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 },
@@ -129,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 },
@@ -139,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,
@@ -148,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);
}
@@ -184,7 +216,7 @@ export function editMember(
);
}
/**
/**
* 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.
@@ -197,3 +229,34 @@ export function moveMember(
) {
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,5 +1,4 @@
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";
@@ -10,6 +9,7 @@ import { Errors } from "../types/errors.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,4 +1,3 @@
import { endpoints } from "../constants/discord.ts";
import { RequestManager } from "../module/requestManager.ts";
import { structures } from "../structures/mod.ts";
import { Errors } from "../types/errors.ts";
@@ -9,6 +8,7 @@ import {
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

@@ -1,6 +1,6 @@
import { endpoints } from "../constants/discord.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 { 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) => {
@@ -317,17 +321,36 @@ function handleStatusCode(response: Response, errorStack?: unknown) {
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) {
@@ -361,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

@@ -90,6 +90,7 @@ export async function handleDiscordPayload(
shardID: number,
) {
eventHandlers.raw?.(data);
await eventHandlers.dispatchRequirements?.(data, shardID);
switch (data.op) {
case GatewayOpcode.HeartbeatACK:

View File

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

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

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

@@ -33,4 +33,8 @@ export enum Errors {
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,3 +1,4 @@
import { Guild } from "../structures/guild.ts";
import { ChannelCreatePayload, ChannelTypes } from "./channel.ts";
import { Emoji, StatusType } from "./discord.ts";
import { MemberCreatePayload } from "./member.ts";
@@ -612,3 +613,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

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

@@ -83,6 +83,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

@@ -82,6 +82,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}`,
@@ -89,6 +92,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

@@ -58,7 +58,8 @@ 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)
@@ -182,7 +183,8 @@ export async function hasChannelPermissions(
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);
const hasPerms = await botHasPermission(guild.id, permissions);
return hasPerms;
}
/** This function converts a bitwise string to permission strings */