mirror of
https://github.com/discordeno/discordeno.git
synced 2026-09-17 16:57:22 +00:00
@@ -23,8 +23,10 @@ jobs:
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'run-tests' }}
|
||||
run: DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} deno test --unstable --coverage=coverage --allow-net tests/mod.ts
|
||||
- name: Create coverage report
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: deno --unstable coverage ./coverage --lcov > coverage.lcov
|
||||
- name: Collect and upload the coverage report
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: codecov/codecov-action@v1.0.10
|
||||
with:
|
||||
file: ./coverage.lcov
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function handleChannelDelete(data: DiscordGatewayPayload) {
|
||||
if (!cachedChannel) return;
|
||||
|
||||
if (
|
||||
cachedChannel.type === DiscordChannelTypes.GUILD_VOICE && payload.guildId
|
||||
cachedChannel.type === DiscordChannelTypes.GuildVoice && payload.guildId
|
||||
) {
|
||||
const guild = await cacheHandlers.get("guilds", cachedChannel.guildId);
|
||||
|
||||
@@ -34,15 +34,27 @@ export async function handleChannelDelete(data: DiscordGatewayPayload) {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
DiscordChannelTypes.GuildText,
|
||||
DiscordChannelTypes.Dm,
|
||||
DiscordChannelTypes.GroupDm,
|
||||
DiscordChannelTypes.GuildNews,
|
||||
].includes(payload.type)
|
||||
) {
|
||||
await cacheHandlers.delete("channels", snowflakeToBigint(payload.id));
|
||||
cacheHandlers.forEach("messages", (message) => {
|
||||
eventHandlers.debug?.(
|
||||
"loop",
|
||||
`Running forEach messages loop in CHANNEL_DELTE file.`,
|
||||
);
|
||||
if (message.channelId === snowflakeToBigint(payload.id)) {
|
||||
cacheHandlers.delete("messages", message.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await cacheHandlers.delete("channels", snowflakeToBigint(payload.id));
|
||||
cacheHandlers.forEach("messages", (message) => {
|
||||
eventHandlers.debug?.(
|
||||
"loop",
|
||||
`Running forEach messages loop in CHANNEL_DELTE file.`,
|
||||
);
|
||||
if (message.channelId === snowflakeToBigint(payload.id)) {
|
||||
cacheHandlers.delete("messages", message.id);
|
||||
}
|
||||
});
|
||||
|
||||
eventHandlers.channelDelete?.(cachedChannel);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@ export async function handleChannelUpdate(data: DiscordGatewayPayload) {
|
||||
"channels",
|
||||
snowflakeToBigint(payload.id),
|
||||
);
|
||||
if (!cachedChannel) return;
|
||||
|
||||
const discordenoChannel = await structures.createDiscordenoChannel(payload);
|
||||
await cacheHandlers.set("channels", discordenoChannel.id, discordenoChannel);
|
||||
|
||||
if (!cachedChannel) return;
|
||||
|
||||
eventHandlers.channelUpdate?.(discordenoChannel, cachedChannel);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { cacheHandlers } from "../../cache.ts";
|
||||
import { structures } from "../../structures/mod.ts";
|
||||
import { Channel } from "../../types/channels/channel.ts";
|
||||
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
|
||||
|
||||
export async function handleThreadCreate(data: DiscordGatewayPayload) {
|
||||
const payload = data.d as Channel;
|
||||
|
||||
const discordenoChannel = await structures.createDiscordenoChannel(payload);
|
||||
await cacheHandlers.set("channels", discordenoChannel.id, discordenoChannel);
|
||||
|
||||
eventHandlers.threadCreate?.(discordenoChannel);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { cacheHandlers } from "../../cache.ts";
|
||||
import { Channel } from "../../types/channels/channel.ts";
|
||||
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
|
||||
import { snowflakeToBigint } from "../../util/bigint.ts";
|
||||
|
||||
export async function handleThreadDelete(data: DiscordGatewayPayload) {
|
||||
const payload = data.d as Channel;
|
||||
|
||||
const cachedChannel = await cacheHandlers.get(
|
||||
"channels",
|
||||
snowflakeToBigint(payload.id),
|
||||
);
|
||||
if (!cachedChannel) return;
|
||||
|
||||
await cacheHandlers.delete("channels", snowflakeToBigint(payload.id));
|
||||
cacheHandlers.forEach("messages", (message) => {
|
||||
eventHandlers.debug?.(
|
||||
"loop",
|
||||
`Running forEach messages loop in CHANNEL_DELTE file.`,
|
||||
);
|
||||
if (message.channelId === snowflakeToBigint(payload.id)) {
|
||||
cacheHandlers.delete("messages", message.id);
|
||||
}
|
||||
});
|
||||
|
||||
eventHandlers.threadDelete?.(cachedChannel);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { cacheHandlers } from "../../cache.ts";
|
||||
import { DiscordenoChannel } from "../../structures/channel.ts";
|
||||
import { structures } from "../../structures/mod.ts";
|
||||
import { ThreadListSync } from "../../types/channels/threads/thread_list_sync.ts";
|
||||
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
|
||||
import { snowflakeToBigint } from "../../util/bigint.ts";
|
||||
import { Collection } from "../../util/collection.ts";
|
||||
|
||||
export async function handleThreadListSync(data: DiscordGatewayPayload) {
|
||||
const payload = data.d as ThreadListSync;
|
||||
|
||||
const discordenoChannels = await Promise.all(
|
||||
payload.threads.map(async (thread) => {
|
||||
const discordenoChannel = await structures.createDiscordenoChannel(
|
||||
thread,
|
||||
snowflakeToBigint(payload.guildId),
|
||||
);
|
||||
|
||||
await cacheHandlers.set(
|
||||
"channels",
|
||||
discordenoChannel.id,
|
||||
discordenoChannel,
|
||||
);
|
||||
|
||||
return discordenoChannel;
|
||||
}),
|
||||
);
|
||||
|
||||
const threads = new Collection<bigint, DiscordenoChannel>(
|
||||
discordenoChannels.map((t) => [t.id, t]),
|
||||
);
|
||||
|
||||
eventHandlers.threadListSync?.(
|
||||
threads,
|
||||
payload.members,
|
||||
snowflakeToBigint(payload.guildId),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { cacheHandlers } from "../../cache.ts";
|
||||
import { ThreadMembersUpdate } from "../../types/channels/threads/thread_members_update.ts";
|
||||
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
|
||||
import { snowflakeToBigint } from "../../util/bigint.ts";
|
||||
|
||||
export async function handleThreadMembersUpdate(data: DiscordGatewayPayload) {
|
||||
const payload = data.d as ThreadMembersUpdate;
|
||||
const thread = await cacheHandlers.get(
|
||||
"channels",
|
||||
snowflakeToBigint(payload.id),
|
||||
);
|
||||
if (!thread) return;
|
||||
|
||||
thread.memberCount = payload.memberCount;
|
||||
await cacheHandlers.set("channels", thread.id, thread);
|
||||
|
||||
eventHandlers.threadMembersUpdate?.(payload);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { cacheHandlers } from "../../cache.ts";
|
||||
import { ThreadMember } from "../../types/channels/threads/thread_member.ts";
|
||||
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
|
||||
import { snowflakeToBigint } from "../../util/bigint.ts";
|
||||
|
||||
export async function handleThreadMemberUpdate(data: DiscordGatewayPayload) {
|
||||
const payload = data.d as ThreadMember;
|
||||
const thread = await cacheHandlers.get(
|
||||
"channels",
|
||||
snowflakeToBigint(payload.id),
|
||||
);
|
||||
if (!thread) return;
|
||||
|
||||
thread.member = payload;
|
||||
|
||||
await cacheHandlers.set("channels", thread.id, thread);
|
||||
|
||||
eventHandlers.threadMemberUpdate?.(payload);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { cacheHandlers } from "../../cache.ts";
|
||||
import { structures } from "../../structures/mod.ts";
|
||||
import { Channel } from "../../types/channels/channel.ts";
|
||||
import { DiscordGatewayPayload } from "../../types/gateway/gateway_payload.ts";
|
||||
import { snowflakeToBigint } from "../../util/bigint.ts";
|
||||
|
||||
export async function handleThreadUpdate(data: DiscordGatewayPayload) {
|
||||
const payload = data.d as Channel;
|
||||
const oldChannel = await cacheHandlers.get(
|
||||
"channels",
|
||||
snowflakeToBigint(payload.id),
|
||||
);
|
||||
if (!oldChannel) return;
|
||||
|
||||
const discordenoChannel = await structures.createDiscordenoChannel(payload);
|
||||
await cacheHandlers.set("channels", discordenoChannel.id, discordenoChannel);
|
||||
|
||||
eventHandlers.threadUpdate?.(discordenoChannel, oldChannel);
|
||||
}
|
||||
@@ -2,6 +2,12 @@ import { handleChannelCreate } from "./channels/CHANNEL_CREATE.ts";
|
||||
import { handleChannelDelete } from "./channels/CHANNEL_DELETE.ts";
|
||||
import { handleChannelPinsUpdate } from "./channels/CHANNEL_PINS_UPDATE.ts";
|
||||
import { handleChannelUpdate } from "./channels/CHANNEL_UPDATE.ts";
|
||||
import { handleThreadCreate } from "./channels/THREAD_CREATE.ts";
|
||||
import { handleThreadDelete } from "./channels/THREAD_DELETE.ts";
|
||||
import { handleThreadListSync } from "./channels/THREAD_LIST_SYNC.ts";
|
||||
import { handleThreadMembersUpdate } from "./channels/THREAD_MEMBERS_UPDATE.ts";
|
||||
import { handleThreadMemberUpdate } from "./channels/THREAD_MEMBER_UPDATE.ts";
|
||||
import { handleThreadUpdate } from "./channels/THREAD_UPDATE.ts";
|
||||
import { handleApplicationCommandCreate } from "./commands/APPLICATION_COMMAND_CREATE.ts";
|
||||
import { handleApplicationCommandDelete } from "./commands/APPLICATION_COMMAND_DELETE.ts";
|
||||
import { handleApplicationCommandUpdate } from "./commands/APPLICATION_COMMAND_UPDATE.ts";
|
||||
@@ -77,6 +83,12 @@ export {
|
||||
handleMessageUpdate,
|
||||
handlePresenceUpdate,
|
||||
handleReady,
|
||||
handleThreadCreate,
|
||||
handleThreadDelete,
|
||||
handleThreadListSync,
|
||||
handleThreadMembersUpdate,
|
||||
handleThreadMemberUpdate,
|
||||
handleThreadUpdate,
|
||||
handleTypingStart,
|
||||
handleUserUpdate,
|
||||
handleVoiceServerUpdate,
|
||||
@@ -92,6 +104,12 @@ export let handlers = {
|
||||
CHANNEL_DELETE: handleChannelDelete,
|
||||
CHANNEL_PINS_UPDATE: handleChannelPinsUpdate,
|
||||
CHANNEL_UPDATE: handleChannelUpdate,
|
||||
THREAD_CREATE: handleThreadCreate,
|
||||
THREAD_UPDATE: handleThreadUpdate,
|
||||
THREAD_DELETE: handleThreadDelete,
|
||||
THREAD_LIST_SYNC: handleThreadListSync,
|
||||
THREAD_MEMBER_UPDATE: handleThreadMemberUpdate,
|
||||
THREAD_MEMBERS_UPDATE: handleThreadMembersUpdate,
|
||||
// commands
|
||||
APPLICATION_COMMAND_CREATE: handleApplicationCommandCreate,
|
||||
APPLICATION_COMMAND_DELETE: handleApplicationCommandDelete,
|
||||
|
||||
@@ -14,8 +14,8 @@ export async function cloneChannel(channelId: bigint, reason?: string) {
|
||||
|
||||
//Check for DM channel
|
||||
if (
|
||||
channelToClone.type === DiscordChannelTypes.DM ||
|
||||
channelToClone.type === DiscordChannelTypes.GROUP_DM
|
||||
channelToClone.type === DiscordChannelTypes.Dm ||
|
||||
channelToClone.type === DiscordChannelTypes.GroupDm
|
||||
) {
|
||||
throw new Error(Errors.CHANNEL_NOT_IN_GUILD);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { cacheHandlers } from "../../cache.ts";
|
||||
import { rest } from "../../rest/rest.ts";
|
||||
import { structures } from "../../structures/mod.ts";
|
||||
@@ -8,11 +7,10 @@ import type {
|
||||
CreateGuildChannel,
|
||||
DiscordCreateGuildChannel,
|
||||
} from "../../types/guilds/create_guild_channel.ts";
|
||||
import type { PermissionStrings } from "../../types/permissions/permission_strings.ts";
|
||||
import { endpoints } from "../../util/constants.ts";
|
||||
import {
|
||||
calculateBits,
|
||||
requireBotGuildPermissions,
|
||||
requireOverwritePermissions,
|
||||
} from "../../util/permissions.ts";
|
||||
import { camelKeysToSnakeCase } from "../../util/utils.ts";
|
||||
|
||||
@@ -22,18 +20,12 @@ export async function createChannel(
|
||||
options?: CreateGuildChannel,
|
||||
reason?: string,
|
||||
) {
|
||||
const requiredPerms: Set<PermissionStrings> = new Set(["MANAGE_CHANNELS"]);
|
||||
|
||||
options?.permissionOverwrites?.forEach((overwrite) => {
|
||||
eventHandlers.debug?.(
|
||||
"loop",
|
||||
`Running forEach loop in create_channel file.`,
|
||||
if (options?.permissionOverwrites) {
|
||||
await requireOverwritePermissions(
|
||||
guildId,
|
||||
options.permissionOverwrites,
|
||||
);
|
||||
overwrite.allow.forEach(requiredPerms.add, requiredPerms);
|
||||
overwrite.deny.forEach(requiredPerms.add, requiredPerms);
|
||||
});
|
||||
|
||||
await requireBotGuildPermissions(guildId, [...requiredPerms]);
|
||||
}
|
||||
|
||||
// BITRATES ARE IN THOUSANDS SO IF USER PROVIDES 32 WE CONVERT TO 32000
|
||||
if (options?.bitrate && options.bitrate < 1000) options.bitrate *= 1000;
|
||||
@@ -48,7 +40,7 @@ export async function createChannel(
|
||||
allow: calculateBits(perm.allow),
|
||||
deny: calculateBits(perm.deny),
|
||||
})),
|
||||
type: options?.type || DiscordChannelTypes.GUILD_TEXT,
|
||||
type: options?.type || DiscordChannelTypes.GuildText,
|
||||
reason,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,26 +1,39 @@
|
||||
import { cacheHandlers } from "../../cache.ts";
|
||||
import { rest } from "../../rest/rest.ts";
|
||||
import { Errors } from "../../types/misc/errors.ts";
|
||||
import { ChannelTypes } from "../../types/mod.ts";
|
||||
import { endpoints } from "../../util/constants.ts";
|
||||
import { requireBotGuildPermissions } from "../../util/permissions.ts";
|
||||
|
||||
/** Delete a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */
|
||||
export async function deleteChannel(
|
||||
guildId: bigint,
|
||||
channelId: bigint,
|
||||
reason?: string,
|
||||
): Promise<undefined> {
|
||||
await requireBotGuildPermissions(guildId, ["MANAGE_CHANNELS"]);
|
||||
) {
|
||||
const channel = await cacheHandlers.get("channels", channelId);
|
||||
|
||||
const guild = await cacheHandlers.get("guilds", guildId);
|
||||
if (!guild) throw new Error(Errors.GUILD_NOT_FOUND);
|
||||
if (channel?.guildId) {
|
||||
const guild = await cacheHandlers.get("guilds", channel.guildId);
|
||||
if (!guild) throw new Error(Errors.GUILD_NOT_FOUND);
|
||||
|
||||
if (guild?.rulesChannelId === channelId) {
|
||||
throw new Error(Errors.RULES_CHANNEL_CANNOT_BE_DELETED);
|
||||
}
|
||||
// TODO(threads): check if this requires guild perms or channel is enough
|
||||
await requireBotGuildPermissions(
|
||||
guild,
|
||||
[
|
||||
ChannelTypes.GuildNewsThread,
|
||||
ChannelTypes.GuildPivateThread,
|
||||
ChannelTypes.GuildPublicThread,
|
||||
].includes(channel.type)
|
||||
? ["MANAGE_THREADS"]
|
||||
: ["MANAGE_CHANNELS"],
|
||||
);
|
||||
if (guild.rulesChannelId === channelId) {
|
||||
throw new Error(Errors.RULES_CHANNEL_CANNOT_BE_DELETED);
|
||||
}
|
||||
|
||||
if (guild?.publicUpdatesChannelId === channelId) {
|
||||
throw new Error(Errors.UPDATES_CHANNEL_CANNOT_BE_DELETED);
|
||||
if (guild.publicUpdatesChannelId === channelId) {
|
||||
throw new Error(Errors.UPDATES_CHANNEL_CANNOT_BE_DELETED);
|
||||
}
|
||||
}
|
||||
|
||||
return await rest.runMethod<undefined>(
|
||||
|
||||
@@ -1,22 +1,68 @@
|
||||
import { eventHandlers } from "../../bot.ts";
|
||||
import { cacheHandlers } from "../../cache.ts";
|
||||
import { rest } from "../../rest/rest.ts";
|
||||
import type { Channel } from "../../types/channels/channel.ts";
|
||||
import { DiscordChannelTypes } from "../../types/channels/channel_types.ts";
|
||||
import type { ModifyChannel } from "../../types/channels/modify_channel.ts";
|
||||
import type { ModifyThread } from "../../types/channels/threads/modify_thread.ts";
|
||||
import type { PermissionStrings } from "../../types/permissions/permission_strings.ts";
|
||||
import { endpoints } from "../../util/constants.ts";
|
||||
import {
|
||||
calculateBits,
|
||||
requireBotChannelPermissions,
|
||||
requireOverwritePermissions,
|
||||
} from "../../util/permissions.ts";
|
||||
import { camelKeysToSnakeCase, hasOwnProperty } from "../../util/utils.ts";
|
||||
|
||||
//TODO: implement DM group channel edit
|
||||
//TODO(threads): check thread perms
|
||||
/** Update a channel's settings. Requires the `MANAGE_CHANNELS` permission for the guild. */
|
||||
export async function editChannel(
|
||||
channelId: bigint,
|
||||
options: ModifyChannel,
|
||||
options: ModifyChannel | ModifyThread,
|
||||
reason?: string,
|
||||
) {
|
||||
await requireBotChannelPermissions(channelId, ["MANAGE_CHANNELS"]);
|
||||
const channel = await cacheHandlers.get("channels", channelId);
|
||||
|
||||
if (options.name || options.topic) {
|
||||
if (channel) {
|
||||
if (
|
||||
[
|
||||
DiscordChannelTypes.GuildNewsThread,
|
||||
DiscordChannelTypes.GuildPivateThread,
|
||||
DiscordChannelTypes.GuildPublicThread,
|
||||
].includes(channel.type)
|
||||
) {
|
||||
const permissions = new Set<PermissionStrings>();
|
||||
|
||||
if (hasOwnProperty(options, "archive") && options.archive === false) {
|
||||
permissions.add("SEND_MESSAGES");
|
||||
}
|
||||
|
||||
// TODO(threads): change this to a better check
|
||||
// hacky way of checking if more is being modified
|
||||
if (Object.keys(options).length > 1) {
|
||||
permissions.add("MANAGE_THREADS");
|
||||
}
|
||||
|
||||
await requireBotChannelPermissions(channel.parentId ?? 0n, [
|
||||
...permissions,
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
hasOwnProperty<ModifyChannel>(
|
||||
options,
|
||||
"permissionOverwrites",
|
||||
) && Array.isArray(options.permissionOverwrites)
|
||||
) {
|
||||
await requireOverwritePermissions(
|
||||
channel.guildId,
|
||||
options.permissionOverwrites,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.name || (options as ModifyChannel).topic) {
|
||||
const request = editChannelNameTopicQueue.get(channelId);
|
||||
if (!request) {
|
||||
// If this hasnt been done before simply add 1 for it
|
||||
@@ -42,21 +88,18 @@ export async function editChannel(
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...options,
|
||||
...camelKeysToSnakeCase<Record<string, unknown>>(options),
|
||||
// deno-lint-ignore camelcase
|
||||
rate_limit_per_user: options.rateLimitPerUser,
|
||||
// deno-lint-ignore camelcase
|
||||
parent_id: options.parentId,
|
||||
// deno-lint-ignore camelcase
|
||||
user_limit: options.userLimit,
|
||||
// deno-lint-ignore camelcase
|
||||
permission_overwrites: options.permissionOverwrites?.map((overwrite) => {
|
||||
return {
|
||||
...overwrite,
|
||||
allow: calculateBits(overwrite.allow),
|
||||
deny: calculateBits(overwrite.deny),
|
||||
};
|
||||
}),
|
||||
permission_overwrites:
|
||||
hasOwnProperty<ModifyChannel>(options, "permissionOverwrites")
|
||||
? options.permissionOverwrites?.map((overwrite) => {
|
||||
return {
|
||||
...overwrite,
|
||||
allow: calculateBits(overwrite.allow),
|
||||
deny: calculateBits(overwrite.deny),
|
||||
};
|
||||
})
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return await rest.runMethod<Channel>(
|
||||
|
||||
@@ -16,9 +16,12 @@ export async function startTyping(channelId: bigint) {
|
||||
if (channel) {
|
||||
if (
|
||||
![
|
||||
DiscordChannelTypes.DM,
|
||||
DiscordChannelTypes.GUILD_NEWS,
|
||||
DiscordChannelTypes.GUILD_TEXT,
|
||||
DiscordChannelTypes.Dm,
|
||||
DiscordChannelTypes.GuildNews,
|
||||
DiscordChannelTypes.GuildText,
|
||||
DiscordChannelTypes.GuildNewsThread,
|
||||
DiscordChannelTypes.GuildPivateThread,
|
||||
DiscordChannelTypes.GuildPublicThread,
|
||||
].includes(channel.type)
|
||||
) {
|
||||
throw new Error(Errors.CHANNEL_NOT_TEXT_BASED);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { cacheHandlers } from "../../../cache.ts";
|
||||
import { rest } from "../../../rest/rest.ts";
|
||||
import { ChannelTypes, Errors } from "../../../types/mod.ts";
|
||||
import { endpoints } from "../../../util/constants.ts";
|
||||
//TODO(threads): this does not work rn
|
||||
/** Adds the current user to a thread. Returns a 204 empty response on success. Also requires the thread is not archived. Fires a Thread Members Update Gateway event.Adds another user to a thread. Requires the ability to send messages in the thread. Also requires the thread is not archived. Returns a 204 empty response on success. Fires a Thread Members Update Gateway event.
|
||||
* @param userId the user to add to the thread defaults to bot
|
||||
*/
|
||||
export async function addToThread(channelId: bigint, userId?: bigint) {
|
||||
// TODO(threads): perm check
|
||||
const channel = await cacheHandlers.get("channels", channelId);
|
||||
if (channel) {
|
||||
if (
|
||||
![
|
||||
ChannelTypes.GuildNewsThread,
|
||||
ChannelTypes.GuildPivateThread,
|
||||
ChannelTypes.GuildPublicThread,
|
||||
].includes(channel.type)
|
||||
) {
|
||||
throw new Error(Errors.NOT_A_THREAD_CHANNEL);
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
"put",
|
||||
userId
|
||||
? endpoints.THREAD_USER(channelId, userId)
|
||||
: endpoints.THREAD_ME(channelId),
|
||||
);
|
||||
return await rest.runMethod(
|
||||
"put",
|
||||
userId
|
||||
? endpoints.THREAD_USER(channelId, userId)
|
||||
: endpoints.THREAD_ME(channelId),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { rest } from "../../../rest/rest.ts";
|
||||
import { endpoints } from "../../../util/constants.ts";
|
||||
|
||||
/** Returns all active threads in the channel, including public and private threads. Threads are ordered by their id, in descending order. Requires the READ_MESSAGE_HISTORY permission. */
|
||||
export async function getActiveThreads(channelId: bigint) {
|
||||
// TODO(threads): perm check
|
||||
// TODO(threads): test if it works
|
||||
return await rest.runMethod(
|
||||
"get",
|
||||
endpoints.THREAD_ACTIVE(channelId),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { rest } from "../../../rest/rest.ts";
|
||||
import { ListPublicArchivedThreads } from "../../../types/channels/threads/list_public_archived_threads.ts";
|
||||
import { endpoints } from "../../../util/constants.ts";
|
||||
import { camelKeysToSnakeCase } from "../../../util/utils.ts";
|
||||
|
||||
export async function getArchivedThreads(
|
||||
channelId: bigint,
|
||||
options?: ListPublicArchivedThreads & {
|
||||
type?: "public" | "private" | "privateJoinedThreads";
|
||||
},
|
||||
) {
|
||||
// TODO(threads): perm check
|
||||
// TODO(threads): check if this works
|
||||
|
||||
return await rest.runMethod(
|
||||
"get",
|
||||
options?.type === "privateJoinedThreads"
|
||||
? endpoints.THREAD_ARCHIVED_PRIVATE_JOINED(channelId)
|
||||
: options?.type === "private"
|
||||
? endpoints.THREAD_ARCHIVED_PRIVATE(channelId)
|
||||
: endpoints.THREAD_ARCHIVED_PUBLIC(channelId),
|
||||
camelKeysToSnakeCase(options ?? {}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { cacheHandlers } from "../../../cache.ts";
|
||||
import { rest } from "../../../rest/rest.ts";
|
||||
import { ChannelTypes } from "../../../types/channels/channel_types.ts";
|
||||
import { Errors } from "../../../types/misc/errors.ts";
|
||||
import { endpoints } from "../../../util/constants.ts";
|
||||
|
||||
// TODO(threads): it seems like the documented return type is wrong
|
||||
/** Returns array of thread members objects that are members of the thread. */
|
||||
export async function getThreadMembers(channelId: bigint) {
|
||||
// TODO(threads): perm check
|
||||
// TODO(threads): intents check
|
||||
const channel = await cacheHandlers.get("channels", channelId);
|
||||
if (channel) {
|
||||
if (
|
||||
![
|
||||
ChannelTypes.GuildNewsThread,
|
||||
ChannelTypes.GuildPivateThread,
|
||||
ChannelTypes.GuildPublicThread,
|
||||
].includes(channel.type)
|
||||
) {
|
||||
throw new Error(Errors.NOT_A_THREAD_CHANNEL);
|
||||
}
|
||||
}
|
||||
|
||||
return await rest.runMethod("get", endpoints.THREAD_MEMBERS(channelId));
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { cacheHandlers } from "../../../cache.ts";
|
||||
import { rest } from "../../../rest/rest.ts";
|
||||
import { ChannelTypes } from "../../../types/channels/channel_types.ts";
|
||||
import { Errors } from "../../../types/misc/errors.ts";
|
||||
import { endpoints } from "../../../util/constants.ts";
|
||||
|
||||
/** Removes another user from a thread. Requires the MANAGE_THREADS permission or that you are the creator of the thread. Also requires the thread is not archived. Returns a 204 empty response on success. Fires a Thread Members Update Gateway event. */
|
||||
export async function removeFromThread(channelId: bigint, userId?: bigint) {
|
||||
// TODO(threads): perm check
|
||||
const channel = await cacheHandlers.get("channels", channelId);
|
||||
if (channel) {
|
||||
if (
|
||||
![
|
||||
ChannelTypes.GuildNewsThread,
|
||||
ChannelTypes.GuildPivateThread,
|
||||
ChannelTypes.GuildPublicThread,
|
||||
].includes(channel.type)
|
||||
) {
|
||||
throw new Error(Errors.NOT_A_THREAD_CHANNEL);
|
||||
}
|
||||
}
|
||||
|
||||
return await rest.runMethod(
|
||||
"delete",
|
||||
userId
|
||||
? endpoints.THREAD_USER(channelId, userId)
|
||||
: endpoints.THREAD_ME(channelId),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { cacheHandlers } from "../../../cache.ts";
|
||||
import { rest } from "../../../rest/rest.ts";
|
||||
import { ChannelTypes } from "../../../types/channels/channel_types.ts";
|
||||
import { StartThread } from "../../../types/channels/threads/start_thread.ts";
|
||||
import { Errors } from "../../../types/misc/errors.ts";
|
||||
import { endpoints } from "../../../util/constants.ts";
|
||||
import { camelKeysToSnakeCase } from "../../../util/utils.ts";
|
||||
|
||||
/**
|
||||
* Creates a new public thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event.
|
||||
* @param messageId when provided the thread will be public
|
||||
*/
|
||||
export async function startThread(
|
||||
channelId: bigint,
|
||||
options: StartThread & { messageId?: bigint },
|
||||
) {
|
||||
const channel = await cacheHandlers.get("channels", channelId);
|
||||
if (channel) {
|
||||
// TODO(threads): perm check
|
||||
if (
|
||||
![ChannelTypes.GuildText, ChannelTypes.GuildNews].includes(channel.type)
|
||||
) {
|
||||
throw new Error(Errors.INVALID_THREAD_PARENT_CHANNEL_TYPE);
|
||||
}
|
||||
|
||||
if (!options.messageId && channel.type === ChannelTypes.GuildNews) {
|
||||
throw new Error(Errors.GUILD_NEWS_CHANNEL_ONLY_SUPPORT_PUBLIC_THREADS);
|
||||
}
|
||||
}
|
||||
|
||||
return rest.runMethod(
|
||||
"post",
|
||||
options?.messageId
|
||||
? endpoints.THREAD_START_PUBLIC(channelId, options.messageId)
|
||||
: endpoints.THREAD_START_PRIVATE(channelId),
|
||||
camelKeysToSnakeCase(options),
|
||||
);
|
||||
}
|
||||
@@ -26,9 +26,12 @@ export async function sendMessage(
|
||||
if (channel) {
|
||||
if (
|
||||
![
|
||||
DiscordChannelTypes.DM,
|
||||
DiscordChannelTypes.GUILD_NEWS,
|
||||
DiscordChannelTypes.GUILD_TEXT,
|
||||
DiscordChannelTypes.Dm,
|
||||
DiscordChannelTypes.GuildNews,
|
||||
DiscordChannelTypes.GuildText,
|
||||
DiscordChannelTypes.GuildPublicThread,
|
||||
DiscordChannelTypes.GuildPivateThread,
|
||||
DiscordChannelTypes.GuildNewsThread,
|
||||
].includes(channel.type)
|
||||
) {
|
||||
throw new Error(Errors.CHANNEL_NOT_TEXT_BASED);
|
||||
|
||||
@@ -62,8 +62,8 @@ const baseChannel: Partial<DiscordenoChannel> = {
|
||||
disconnect(memberId) {
|
||||
return disconnectMember(this.guildId!, memberId);
|
||||
},
|
||||
delete() {
|
||||
return deleteChannel(this.guildId!, this.id!);
|
||||
delete(reason) {
|
||||
return deleteChannel(this.id!, reason);
|
||||
},
|
||||
editOverwrite(id, options) {
|
||||
return editChannelOverwrite(
|
||||
@@ -204,7 +204,7 @@ export interface DiscordenoChannel extends
|
||||
/** Disconnect a member from a voice channel. Requires MOVE_MEMBERS permission. */
|
||||
disconnect(memberId: bigint): ReturnType<typeof disconnectMember>;
|
||||
/** Delete the channel */
|
||||
delete(): ReturnType<typeof deleteChannel>;
|
||||
delete(reason?: string): ReturnType<typeof deleteChannel>;
|
||||
/** Edit a channel Overwrite */
|
||||
editOverwrite(
|
||||
overwriteId: bigint,
|
||||
|
||||
@@ -158,6 +158,7 @@ export async function createDiscordenoGuild(
|
||||
memberCount = 0,
|
||||
voiceStates = [],
|
||||
channels = [],
|
||||
threads = [],
|
||||
presences = [],
|
||||
joinedAt = "",
|
||||
emojis,
|
||||
@@ -181,7 +182,7 @@ export async function createDiscordenoGuild(
|
||||
voiceStates.map((vs) => structures.createDiscordenoVoiceState(guildId, vs)),
|
||||
);
|
||||
|
||||
await Promise.all(channels.map(async (channel) => {
|
||||
await Promise.all([...channels, ...threads].map(async (channel) => {
|
||||
const discordenoChannel = await structures.createDiscordenoChannel(
|
||||
channel,
|
||||
guildId,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { User } from "../users/user.ts";
|
||||
import { DiscordChannelTypes } from "./channel_types.ts";
|
||||
import { DiscordOverwrite } from "./overwrite.ts";
|
||||
import { ThreadMember } from "./threads/thread_member.ts";
|
||||
import { ThreadMetadata } from "./threads/thread_metadata.ts";
|
||||
import { DiscordVideoQualityModes } from "./video_quality_modes.ts";
|
||||
|
||||
/** https://discord.com/developers/docs/resources/channel#channel-object */
|
||||
@@ -33,11 +35,11 @@ export interface Channel {
|
||||
recipients?: User[];
|
||||
/** Icon hash */
|
||||
icon?: string | null;
|
||||
/** id of the DM creator */
|
||||
/** Id of the creator of the group DM or thread */
|
||||
ownerId?: string;
|
||||
/** Application id of the group DM creator if it is bot-created */
|
||||
applicationId?: string;
|
||||
/** Id of the parent category for a channel (each parent category can contain up to 50 channels) */
|
||||
/** For guild channels: Id of the parent category for a channel (each parent category can contain up to 50 channels), for threads: id of the text channel this thread was created */
|
||||
parentId?: string | null;
|
||||
/** When the last pinned message was pinned. This may be null in events such as GUILD_CREATE when a message is not pinned. */
|
||||
lastPinTimestamp?: string | null;
|
||||
@@ -45,4 +47,13 @@ export interface Channel {
|
||||
rtcRegion?: string | null;
|
||||
/** The camera video quality mode of the voice channel, 1 when not present */
|
||||
videoQualityMode?: DiscordVideoQualityModes;
|
||||
// TODO(threads): consider a ThreadChannel object
|
||||
/** An approximate count of messages in a thread, stops counting at 50 */
|
||||
messageCount?: number;
|
||||
/** An approximate count of users in a thread, stops counting at 50 */
|
||||
memberCount?: number;
|
||||
/** Thread-specifig fields not needed by other channels */
|
||||
threadMetadata?: ThreadMetadata;
|
||||
/** Thread member object for the current user, if they have joined the thread, only included on certain API endpoints */
|
||||
member?: ThreadMember;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
/** https://discord.com/developers/docs/resources/channel#channel-object-channel-types */
|
||||
export enum DiscordChannelTypes {
|
||||
/** A text channel within a server */
|
||||
GUILD_TEXT,
|
||||
GuildText,
|
||||
/** A direct message between users */
|
||||
DM,
|
||||
Dm,
|
||||
/** A voice channel within a server */
|
||||
GUILD_VOICE,
|
||||
GuildVoice,
|
||||
/** A direct message between multiple users */
|
||||
GROUP_DM,
|
||||
GroupDm,
|
||||
/** An organizational category that contains up to 50 channels */
|
||||
GUILD_CATEGORY,
|
||||
GuildCategory,
|
||||
/** A channel that users can follow and crosspost into their own server */
|
||||
GUILD_NEWS,
|
||||
GuildNews,
|
||||
/** A channel in which game developers can sell their game on Discord */
|
||||
GUILD_STORE,
|
||||
GuildStore,
|
||||
/** A temporary sub-channel within a GUILD_NEWS channel */
|
||||
GuildNewsThread = 10,
|
||||
/** A temporary sub-channel within a GUILD_TEXT channel */
|
||||
GuildPublicThread,
|
||||
/** A temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission */
|
||||
GuildPivateThread,
|
||||
/** A voice channel for hosting events with an audience */
|
||||
GUILD_STAGE_VOICE = 13,
|
||||
GuildStageVoice = 13,
|
||||
}
|
||||
|
||||
export type ChannelTypes = DiscordChannelTypes;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Channel } from "../channel.ts";
|
||||
import { ThreadMember } from "./thread_member.ts";
|
||||
|
||||
// TODO: add docs link
|
||||
export interface ListActiveThreads {
|
||||
/** The active threads */
|
||||
threads: Channel[];
|
||||
/** A thread member object for each returned thread the current user has joined */
|
||||
members: ThreadMember[];
|
||||
/** Whether there are potentially additional threads that could be returned on subsequent call */
|
||||
hasMore: boolean;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// TODO: add docs link
|
||||
export interface ListPublicArchivedThreads {
|
||||
// TODO: convert unix to ISO9601 timestamp
|
||||
/** Returns threads before this timestamp. UNIX or ISO8601 timestamp */
|
||||
before?: number | string;
|
||||
/** Optional maximum number of threads to return */
|
||||
limit?: number;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// TODO: add docs link
|
||||
export interface ModifyThread {
|
||||
/** 2-100 character thread name */
|
||||
name?: string;
|
||||
/** Whether the thread is archived */
|
||||
archived?: boolean;
|
||||
/** Duration in minutes to automatically archive the thread after recent activity */
|
||||
autoArchiveDuration?: 60 | 1440 | 4320 | 10080;
|
||||
/** When a thread is locked, only users with `MANAGE_THREADS` can unarchive it */
|
||||
locked?: boolean;
|
||||
/** Amount of seconds a user has to wait before sending another message (0-21600); bots, as well as users with the permission `MANAGE_MESSAGES`, `MANAGE_THREAD` or `MANAGE_CHANNEL` are unaffected */
|
||||
rateLimitPerUser?: number;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// TODO: add docs link
|
||||
export interface StartThread {
|
||||
/** 2-100 character thread name */
|
||||
name: string;
|
||||
/** Duration in minutes to automatically archive the thread after recent activity */
|
||||
autoArchiveDuration: 60 | 1440 | 4320 | 10080;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Channel } from "../channel.ts";
|
||||
import { ThreadMember } from "./thread_member.ts";
|
||||
|
||||
// TODO: add docs link
|
||||
export interface ThreadListSync {
|
||||
/** The id of the guild */
|
||||
guildId: string;
|
||||
/** The parent channel ids whose threads are being synced. If omitted, then threads were synced for the entire guild. This array may contain channelIds that have no active threads as well, so you know to clear that data */
|
||||
channelIds?: string[];
|
||||
// TODO: check if need to omit
|
||||
/** All active threads in the given channels that the current user can access */
|
||||
threads: Channel[];
|
||||
/** All thread member objects from the synced threads for the current user, indicating which threads the current user has been added to */
|
||||
members: ThreadMember[];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export interface ThreadMember {
|
||||
/** The id of the thread */
|
||||
id: string;
|
||||
/** The id of the user */
|
||||
userId: string;
|
||||
/** The time the current user last joined the thread */
|
||||
joinTimestamp: string;
|
||||
/** Any user-thread settings, currently only used for notifications */
|
||||
flags: number;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ThreadMember } from "./thread_member.ts";
|
||||
|
||||
// TODO: add docs link
|
||||
export interface ThreadMembersUpdate {
|
||||
/** The id of the thread */
|
||||
id: string;
|
||||
/** The id of the guild */
|
||||
guildId: string;
|
||||
/** The approximate number of members in the thread, capped at 50 */
|
||||
memberCount: number;
|
||||
/** The users who were added to the thread */
|
||||
addedMembers?: ThreadMember[];
|
||||
/** The id of the users who were removed from the thread */
|
||||
removedMemberIds?: string[];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface ThreadMetadata {
|
||||
/** Whether the thread is archived */
|
||||
archived: boolean;
|
||||
/** Id of the user that last archived or unarchived the thread */
|
||||
archiverId?: string;
|
||||
/** Duration in minutes to automatically archive the thread after recent activity */
|
||||
autoArchiveDuration: 60 | 1440 | 4320 | 10080;
|
||||
// TODO(threads): channel struct should convert this to a unixx
|
||||
/** Timestamp when the thread's archive status was last changed, used for calculating recent activity */
|
||||
archiveTimestamp: string;
|
||||
/** When a thread is locked, only users with `MANAGE_THREADS` can unarchive it */
|
||||
locked?: boolean;
|
||||
}
|
||||
@@ -80,6 +80,11 @@ export enum DiscordJsonErrorCodes {
|
||||
InvalidApiVersionProvided = 50041,
|
||||
CannotDeleteAChannelRequiredForCommunityGuilds = 50074,
|
||||
InvalidStickerSent = 50081,
|
||||
TriedToPerformAnOperationOnAnArchivedThreadSuchAsEditingAMessageOrAddingAUserToTheThread =
|
||||
50083,
|
||||
InvalidThreadNotificationSettings,
|
||||
BeforeValueIsEarlierThanTheThreadCreationDate,
|
||||
TwoFactorIsRequiredForThisOperation = 60003,
|
||||
ReqctionWasBlocked = 90001,
|
||||
ApiResourceIsCurrentlyOverloadedTryAgainALittleLater = 130000,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DiscordenoMember } from "../../structures/member.ts";
|
||||
import { DiscordenoMessage } from "../../structures/message.ts";
|
||||
import { DiscordenoRole } from "../../structures/role.ts";
|
||||
import { Collection } from "../../util/collection.ts";
|
||||
import { ThreadMember } from "../channels/threads/thread_member.ts";
|
||||
import { ThreadMembersUpdate } from "../channels/threads/thread_members_update.ts";
|
||||
import { IntegrationCreateUpdate } from "../integration/integration_create_update.ts";
|
||||
import { ApplicationCommandCreateUpdateDelete } from "../interactions/application_command_create_update_delete.ts";
|
||||
import {
|
||||
@@ -211,6 +213,25 @@ export interface EventHandlers {
|
||||
shardId: number,
|
||||
unavailableGuildIds: Set<bigint>,
|
||||
) => unknown;
|
||||
/** Sent when a thread is created */
|
||||
threadCreate?: (channel: DiscordenoChannel) => unknown;
|
||||
/** Sent when a thread is updated */
|
||||
threadUpdate?: (
|
||||
cahnnel: DiscordenoChannel,
|
||||
oldChannel: DiscordenoChannel,
|
||||
) => unknown;
|
||||
/** Sent when the bot gains access to threads */
|
||||
threadListSync?: (
|
||||
channels: Collection<bigint, DiscordenoChannel>,
|
||||
members: ThreadMember[],
|
||||
guildId: bigint,
|
||||
) => unknown;
|
||||
/** Sent when the current users thread member is updated */
|
||||
threadMemberUpdate?: (threadMember: ThreadMember) => unknown;
|
||||
/** Sent when anyone is added to or removed from a thread */
|
||||
threadMembersUpdate?: (update: ThreadMembersUpdate) => unknown;
|
||||
/** Sent when a thread is deleted */
|
||||
threadDelete?: (channel: DiscordenoChannel) => unknown;
|
||||
/** Sent when a user starts typing in a channel. */
|
||||
typingStart?: (data: TypingStart) => unknown;
|
||||
/** Sent when a user joins a voice channel */
|
||||
|
||||
@@ -79,6 +79,9 @@ export interface Guild {
|
||||
members?: GuildMember[];
|
||||
/** Channels in the guild */
|
||||
channels?: Channel[];
|
||||
// TODO: check if need to omit
|
||||
/** All active threads in the guild that the current user has permission to view */
|
||||
threads?: Channel[];
|
||||
/** Presences of the members in the guild, will only include non-offline members if the size is greater than large threshold */
|
||||
presences?: Partial<PresenceUpdate>[];
|
||||
/** The maximum number of presences for the guild (the default value, currently 25000, is in effect when null is returned) */
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Channel } from "../channels/channel.ts";
|
||||
import { ChannelMention } from "../channels/channel_mention.ts";
|
||||
import { ThreadMember } from "../channels/threads/thread_member.ts";
|
||||
import { Embed } from "../embeds/embed.ts";
|
||||
import { GuildMember } from "../guilds/guild_member.ts";
|
||||
import { MessageInteraction } from "../interactions/message_interaction.ts";
|
||||
@@ -83,6 +85,8 @@ export interface Message {
|
||||
referencedMessage?: Message;
|
||||
/** Sent if the message is a response to an Interaction */
|
||||
interaction?: MessageInteraction;
|
||||
/** The thread that was started from this message, includes thread member object */
|
||||
thread?: Omit<Channel, "member"> & { member: ThreadMember };
|
||||
/** The components related to this message */
|
||||
components: MessageComponents;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
/** https://discord.com/developers/docs/resources/channel#message-object-message-flags */
|
||||
export enum DiscordMessageFlags {
|
||||
/** This message has been published to subscribed channels (via Channel Following) */
|
||||
CROSSPOSTED = 1 << 0,
|
||||
Crossposted = 1 << 0,
|
||||
/** This message originated from a message in another channel (via Channel Following) */
|
||||
IS_CROSSPOST = 1 << 1,
|
||||
IsCrosspost = 1 << 1,
|
||||
/** Do not include any embeds when serializing this message */
|
||||
SUPPRESS_EMBEDS = 1 << 2,
|
||||
SuppressEmbeeds = 1 << 2,
|
||||
/** The source message for this crosspost has been deleted (via Channel Following) */
|
||||
SOURCE_MESSAGE_DELETED = 1 << 3,
|
||||
SourceMessageDeleted = 1 << 3,
|
||||
/** This message came from the urgent message system */
|
||||
URGENT = 1 << 4,
|
||||
Urgent = 1 << 4,
|
||||
/** This message has an associated thread, with the same id as the message */
|
||||
HasThread = 1 << 5,
|
||||
/** This message is only visible to the user who invoked the Interaction */
|
||||
EMPHERAL = 1 << 6,
|
||||
Empheral = 1 << 6,
|
||||
/** This message is an Interaction Response and the bot is "thinking" */
|
||||
LOADING = 1 << 7,
|
||||
Loading = 1 << 7,
|
||||
}
|
||||
|
||||
export type MessageFlags = DiscordMessageFlags;
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
/** https://discord.com/developers/docs/resources/channel#message-object-message-types */
|
||||
export enum DiscordMessageTypes {
|
||||
DEFAULT,
|
||||
RECIPIENT_ADD,
|
||||
RECIPIENT_REMOVE,
|
||||
CALL,
|
||||
CHANNEL_NAME_CHANGE,
|
||||
CHANNEL_ICON_CHANGE,
|
||||
CHANNEL_PINNED_MESSAGE,
|
||||
GUILD_MEMBER_JOIN,
|
||||
USER_PREMIUM_GUILD_SUBSCRIPTION,
|
||||
USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1,
|
||||
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,
|
||||
APPLICATION_COMMAND,
|
||||
GUILD_INVITE_REMINDER = 22,
|
||||
Default,
|
||||
RecipientAdd,
|
||||
RecipientRemove,
|
||||
Call,
|
||||
ChannelNameChange,
|
||||
ChannelIconChange,
|
||||
ChannelPinnedMessage,
|
||||
GuildMemberJoin,
|
||||
UserPremiumGuildSubscription,
|
||||
UserPremiumGuildSubscriptionTier1,
|
||||
UserPremiumGuildSubscriptionTier2,
|
||||
UserPremiumGuildSubscriptionTier3,
|
||||
ChannelFollowAdd,
|
||||
GuildDiscoveryDisqualified = 14,
|
||||
GuildDiscoveryRequalified,
|
||||
GuildDiscoveryGracePeriodInitialWarning,
|
||||
GuildDiscoveryGracePeriodFinalWarning,
|
||||
ThreadCreated,
|
||||
Reply = 19,
|
||||
ApplicationCommand,
|
||||
GuildInviteReminder = 22,
|
||||
}
|
||||
|
||||
export type MessageTypes = DiscordMessageTypes;
|
||||
|
||||
@@ -18,6 +18,11 @@ export enum Errors {
|
||||
MEMBER_SEARCH_LIMIT_TOO_LOW = "MEMBER_SEARCH_LIMIT_TOO_LOW",
|
||||
PRUNE_MAX_DAYS = "PRUNE_MAX_DAYS",
|
||||
ROLE_NOT_FOUND = "ROLE_NOT_FOUND",
|
||||
// Thread errors
|
||||
INVALID_THREAD_PARENT_CHANNEL_TYPE = "INVALID_THREAD_PARENT_CHANNEL_TYPE",
|
||||
GUILD_NEWS_CHANNEL_ONLY_SUPPORT_PUBLIC_THREADS =
|
||||
"GUILD_NEWS_CHANNEL_ONLY_SUPPORT_PUBLIC_THREADS",
|
||||
NOT_A_THREAD_CHANNEL = "NOT_A_THREAD_CHANNEL",
|
||||
// Message Delete Errors
|
||||
DELETE_MESSAGES_MIN = "DELETE_MESSAGES_MIN",
|
||||
PRUNE_MIN_DAYS = "PRUNE_MIN_DAYS",
|
||||
|
||||
@@ -65,7 +65,13 @@ export enum DiscordBitwisePermissionFlags {
|
||||
/** Allows members to use slash commands in text channels */
|
||||
USE_SLASH_COMMANDS = 0x80000000,
|
||||
/** Allows for requesting to speak in stage channels. */
|
||||
REQUEST_TO_SPEAK = 0x100000000,
|
||||
REQUEST_TO_SPEAK = 0x100000001,
|
||||
/** Allows for deleting and archiving threads, and viewing all private threads */
|
||||
MANAGE_THREADS = 0x0400000000,
|
||||
/** Allows for creating and participating in threads */
|
||||
USE_PUBLIC_THREADS = 0x0800000000,
|
||||
/** Allows for creating and participating in private threads */
|
||||
USE_PRIVATE_THREADS = 0x1000000000,
|
||||
}
|
||||
|
||||
export type BitwisePermissions = DiscordBitwisePermissionFlags;
|
||||
|
||||
+26
-3
@@ -2,13 +2,14 @@
|
||||
export const BASE_URL = "https://discord.com/api";
|
||||
|
||||
/** https://discord.com/developers/docs/reference#api-versioning-api-versions */
|
||||
export const API_VERSION = 8;
|
||||
export const API_VERSION = 9;
|
||||
|
||||
/** https://discord.com/developers/docs/topics/gateway#gateways-gateway-versions */
|
||||
export const GATEWAY_VERSION = 8;
|
||||
export const GATEWAY_VERSION = 9;
|
||||
|
||||
// TODO: update this version
|
||||
/** https://github.com/discordeno/discordeno/releases */
|
||||
export const DISCORDENO_VERSION = 11;
|
||||
export const DISCORDENO_VERSION = "11.0.0";
|
||||
|
||||
/** https://discord.com/developers/docs/reference#user-agent */
|
||||
export const USER_AGENT =
|
||||
@@ -77,6 +78,28 @@ export const endpoints = {
|
||||
// Bots SHALL NOT use this endpoint but they can
|
||||
CHANNEL_TYPING: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/typing`,
|
||||
|
||||
// Thread Endpoints
|
||||
THREAD_START_PUBLIC: (channelId: bigint, messageId: bigint) =>
|
||||
`${endpoints.CHANNEL_MESSAGE(channelId, messageId)}/threads`,
|
||||
THREAD_START_PRIVATE: (channelId: bigint) =>
|
||||
`${CHANNEL_BASE(channelId)}/threads`,
|
||||
THREAD_ACTIVE: (channelId: bigint) =>
|
||||
`${CHANNEL_BASE(channelId)}/threads/active`,
|
||||
THREAD_MEMBERS: (channelId: bigint) =>
|
||||
`${CHANNEL_BASE(channelId)}/thread-members`,
|
||||
THREAD_ME: (channelId: bigint) =>
|
||||
`${endpoints.THREAD_MEMBERS(channelId)}/@me`,
|
||||
THREAD_USER: (channelId: bigint, userId: bigint) =>
|
||||
`${endpoints.THREAD_MEMBERS(channelId)}/${userId}`,
|
||||
THREAD_ARCHIVED_BASE: (channelId: bigint) =>
|
||||
`${CHANNEL_BASE(channelId)}/threads/archived`,
|
||||
THREAD_ARCHIVED_PUBLIC: (channelId: bigint) =>
|
||||
`${endpoints.THREAD_ARCHIVED_BASE(channelId)}/public`,
|
||||
THREAD_ARCHIVED_PRIVATE: (channelId: bigint) =>
|
||||
`${endpoints.THREAD_ARCHIVED_BASE(channelId)}/private`,
|
||||
THREAD_ARCHIVED_PRIVATE_JOINED: (channelId: bigint) =>
|
||||
`${CHANNEL_BASE(channelId)}/users/@me/threads/archived/private`,
|
||||
|
||||
// Guild Endpoints
|
||||
GUILDS: `${baseEndpoints.BASE_URL}/guilds`,
|
||||
GUILD_AUDIT_LOGS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/audit-logs`,
|
||||
|
||||
+27
-11
@@ -4,6 +4,7 @@ import { DiscordenoChannel } from "../structures/channel.ts";
|
||||
import { DiscordenoGuild } from "../structures/guild.ts";
|
||||
import { DiscordenoMember } from "../structures/member.ts";
|
||||
import { DiscordenoRole } from "../structures/role.ts";
|
||||
import { Overwrite } from "../types/channels/overwrite.ts";
|
||||
import { Errors } from "../types/misc/errors.ts";
|
||||
import { DiscordBitwisePermissionFlags } from "../types/permissions/bitwise_permission_flags.ts";
|
||||
import type { PermissionStrings } from "../types/permissions/permission_strings.ts";
|
||||
@@ -28,11 +29,6 @@ async function getCached(
|
||||
? // @ts-ignore TS is wrong here
|
||||
await cacheHandlers.get(table, key)
|
||||
: key;
|
||||
if (!cached || typeof cached === "bigint") {
|
||||
throw new Error(
|
||||
Errors[`${table.slice(0, -1).toUpperCase()}_NOT_FOUND` as Errors],
|
||||
);
|
||||
}
|
||||
|
||||
return typeof cached === "bigint" ? undefined : cached;
|
||||
}
|
||||
@@ -50,7 +46,7 @@ export async function calculateBasePermissions(
|
||||
let permissions = 0n;
|
||||
// Calculate the role permissions bits, @everyone role is not in memberRoleIds so we need to pass guildId manualy
|
||||
permissions |= [...(member.guilds.get(guild.id)?.roles || []), guild.id]
|
||||
.map((id) => (guild as DiscordenoGuild).roles.get(id)?.permissions)
|
||||
.map((id) => guild.roles.get(id)?.permissions)
|
||||
// Removes any edge case undefined
|
||||
.filter((perm) => perm)
|
||||
.reduce((bits, perms) => {
|
||||
@@ -76,7 +72,7 @@ export async function calculateChannelOverwrites(
|
||||
|
||||
const member = await getCached("members", memberOrId);
|
||||
|
||||
if (!member) return "8";
|
||||
if (!channel || !member) return "8";
|
||||
|
||||
// Get all the role permissions this member already has
|
||||
let permissions = BigInt(
|
||||
@@ -84,8 +80,8 @@ export async function calculateChannelOverwrites(
|
||||
);
|
||||
|
||||
// First calculate @everyone overwrites since these have the lowest priority
|
||||
const overwriteEveryone = channel?.permissionOverwrites?.find(
|
||||
(overwrite) => overwrite.id === (channel as DiscordenoChannel).guildId,
|
||||
const overwriteEveryone = channel.permissionOverwrites?.find(
|
||||
(overwrite) => overwrite.id === channel.guildId,
|
||||
);
|
||||
if (overwriteEveryone) {
|
||||
// First remove denied permissions since denied < allowed
|
||||
@@ -93,7 +89,7 @@ export async function calculateChannelOverwrites(
|
||||
permissions |= BigInt(overwriteEveryone.allow);
|
||||
}
|
||||
|
||||
const overwrites = channel?.permissionOverwrites;
|
||||
const overwrites = channel.permissionOverwrites;
|
||||
|
||||
// In order to calculate the role permissions correctly we need to temporarily save the allowed and denied permissions
|
||||
let allow = 0n;
|
||||
@@ -112,7 +108,7 @@ export async function calculateChannelOverwrites(
|
||||
|
||||
// Third calculate member specific overwrites since these have the highest priority
|
||||
const overwriteMember = overwrites?.find(
|
||||
(overwrite) => overwrite.id === (member as DiscordenoMember).id,
|
||||
(overwrite) => overwrite.id === member.id,
|
||||
);
|
||||
if (overwriteMember) {
|
||||
permissions &= ~BigInt(overwriteMember.deny);
|
||||
@@ -290,6 +286,26 @@ export function calculateBits(permissions: PermissionStrings[]) {
|
||||
.toString();
|
||||
}
|
||||
|
||||
/** Internal function to check if the bot has the permissions to set these overwrites */
|
||||
export async function requireOverwritePermissions(
|
||||
guildOrId: bigint | DiscordenoGuild,
|
||||
overwrites: Overwrite[],
|
||||
) {
|
||||
let requiredPerms: Set<PermissionStrings> = new Set(["MANAGE_CHANNELS"]);
|
||||
|
||||
overwrites?.forEach((overwrite) => {
|
||||
overwrite.allow.forEach(requiredPerms.add, requiredPerms);
|
||||
overwrite.deny.forEach(requiredPerms.add, requiredPerms);
|
||||
});
|
||||
|
||||
// MANAGE_ROLES permission can only be set by administrators
|
||||
if (requiredPerms.has("MANAGE_ROLES")) {
|
||||
requiredPerms = new Set<PermissionStrings>(["ADMINISTRATOR"]);
|
||||
}
|
||||
|
||||
await requireGuildPermissions(guildOrId, botId, [...requiredPerms]);
|
||||
}
|
||||
|
||||
/** Gets the highest role from the member in this guild */
|
||||
export async function highestRole(
|
||||
guildOrId: bigint | DiscordenoGuild,
|
||||
|
||||
@@ -204,3 +204,15 @@ export function validateSlashCommands(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Typescript is not so good as we developers so we need this little utility function to help it out
|
||||
// Taken from https://fettblog.eu/typescript-hasownproperty/
|
||||
/** TS save way to check if a property exists in an object */
|
||||
// deno-lint-ignore ban-types
|
||||
export function hasOwnProperty<T extends {}, Y extends PropertyKey = string>(
|
||||
obj: T,
|
||||
prop: Y,
|
||||
): obj is T & Record<Y, unknown> {
|
||||
// deno-lint-ignore no-prototype-builtins
|
||||
return obj.hasOwnProperty(prop);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { defaultTestOptions, tempData } from "../ws/start_bot.ts";
|
||||
import { assertExists } from "../deps.ts";
|
||||
import { cache } from "../../src/cache.ts";
|
||||
import { categoryChildren, createChannel } from "../../src/helpers/mod.ts";
|
||||
import { DiscordChannelTypes } from "../../src/types/channels/channel_types.ts";
|
||||
import { delayUntil } from "../util/delay_until.ts";
|
||||
import { bigintToSnowflake } from "../../src/util/bigint.ts";
|
||||
import { assertExists } from "../deps.ts";
|
||||
import { delayUntil } from "../util/delay_until.ts";
|
||||
import { defaultTestOptions, tempData } from "../ws/start_bot.ts";
|
||||
|
||||
Deno.test({
|
||||
name: "[channel] category channel ids",
|
||||
async fn() {
|
||||
const category = await createChannel(tempData.guildId, {
|
||||
name: "Discordeno-test",
|
||||
type: DiscordChannelTypes.GUILD_CATEGORY,
|
||||
type: DiscordChannelTypes.GuildCategory,
|
||||
});
|
||||
|
||||
// Assertions
|
||||
|
||||
@@ -15,7 +15,7 @@ async function ifItFailsBlameWolf(options: CreateGuildChannel) {
|
||||
|
||||
// Assertions
|
||||
assertExists(channel);
|
||||
assertEquals(channel.type, options.type || DiscordChannelTypes.GUILD_TEXT);
|
||||
assertEquals(channel.type, options.type || DiscordChannelTypes.GuildText);
|
||||
|
||||
// Delay the execution by 5 seconds to allow CHANNEL_CREATE event to be processed
|
||||
await delayUntil(10000, () => cache.channels.has(channel.id));
|
||||
|
||||
@@ -61,7 +61,7 @@ Deno.test({
|
||||
await ifItFailsBlameWolf(
|
||||
{
|
||||
name: "Discordeno-clone-test",
|
||||
type: DiscordChannelTypes.GUILD_CATEGORY,
|
||||
type: DiscordChannelTypes.GuildCategory,
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -74,7 +74,7 @@ Deno.test({
|
||||
await ifItFailsBlameWolf(
|
||||
{
|
||||
name: "Discordeno-clone-test",
|
||||
type: DiscordChannelTypes.GUILD_VOICE,
|
||||
type: DiscordChannelTypes.GuildVoice,
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -87,7 +87,7 @@ Deno.test({
|
||||
await ifItFailsBlameWolf(
|
||||
{
|
||||
name: "discordeno-clone-test",
|
||||
type: DiscordChannelTypes.GUILD_VOICE,
|
||||
type: DiscordChannelTypes.GuildVoice,
|
||||
bitrate: 32000,
|
||||
},
|
||||
);
|
||||
@@ -101,7 +101,7 @@ Deno.test({
|
||||
await ifItFailsBlameWolf(
|
||||
{
|
||||
name: "Discordeno-clone-test",
|
||||
type: DiscordChannelTypes.GUILD_VOICE,
|
||||
type: DiscordChannelTypes.GuildVoice,
|
||||
userLimit: 32,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { defaultTestOptions, tempData } from "../ws/start_bot.ts";
|
||||
import { assertEquals, assertExists } from "../deps.ts";
|
||||
import { cache } from "../../src/cache.ts";
|
||||
import { DiscordChannelTypes } from "../../src/types/channels/channel_types.ts";
|
||||
import { CreateGuildChannel } from "../../src/types/guilds/create_guild_channel.ts";
|
||||
import { createChannel } from "../../src/helpers/channels/create_channel.ts";
|
||||
import { delayUntil } from "../util/delay_until.ts";
|
||||
import { DiscordOverwriteTypes } from "../../src/types/channels/overwrite_types.ts";
|
||||
import { botId } from "../../src/bot.ts";
|
||||
import { cache } from "../../src/cache.ts";
|
||||
import { createChannel } from "../../src/helpers/channels/create_channel.ts";
|
||||
import { DiscordChannelTypes } from "../../src/types/channels/channel_types.ts";
|
||||
import { DiscordOverwriteTypes } from "../../src/types/channels/overwrite_types.ts";
|
||||
import { CreateGuildChannel } from "../../src/types/guilds/create_guild_channel.ts";
|
||||
import { bigintToSnowflake } from "../../src/util/bigint.ts";
|
||||
import { assertEquals, assertExists } from "../deps.ts";
|
||||
import { delayUntil } from "../util/delay_until.ts";
|
||||
import { defaultTestOptions, tempData } from "../ws/start_bot.ts";
|
||||
|
||||
async function ifItFailsBlameWolf(options: CreateGuildChannel, save = false) {
|
||||
const channel = await createChannel(tempData.guildId, options);
|
||||
|
||||
// Assertions
|
||||
assertExists(channel);
|
||||
assertEquals(channel.type, options.type || DiscordChannelTypes.GUILD_TEXT);
|
||||
assertEquals(channel.type, options.type || DiscordChannelTypes.GuildText);
|
||||
|
||||
if (save) tempData.channelId = channel.id;
|
||||
|
||||
@@ -61,7 +61,7 @@ Deno.test({
|
||||
await ifItFailsBlameWolf(
|
||||
{
|
||||
name: "Discordeno-test",
|
||||
type: DiscordChannelTypes.GUILD_CATEGORY,
|
||||
type: DiscordChannelTypes.GuildCategory,
|
||||
},
|
||||
true,
|
||||
);
|
||||
@@ -91,7 +91,7 @@ Deno.test({
|
||||
await ifItFailsBlameWolf(
|
||||
{
|
||||
name: "Discordeno-test",
|
||||
type: DiscordChannelTypes.GUILD_VOICE,
|
||||
type: DiscordChannelTypes.GuildVoice,
|
||||
},
|
||||
true,
|
||||
);
|
||||
@@ -105,7 +105,7 @@ Deno.test({
|
||||
await ifItFailsBlameWolf(
|
||||
{
|
||||
name: "discordeno-test",
|
||||
type: DiscordChannelTypes.GUILD_VOICE,
|
||||
type: DiscordChannelTypes.GuildVoice,
|
||||
bitrate: 32000,
|
||||
},
|
||||
true,
|
||||
@@ -120,7 +120,7 @@ Deno.test({
|
||||
await ifItFailsBlameWolf(
|
||||
{
|
||||
name: "Discordeno-test",
|
||||
type: DiscordChannelTypes.GUILD_VOICE,
|
||||
type: DiscordChannelTypes.GuildVoice,
|
||||
userLimit: 32,
|
||||
},
|
||||
true,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cache } from "../../src/cache.ts";
|
||||
import { createChannel } from "../../src/helpers/channels/create_channel.ts";
|
||||
import { deleteChannel } from "../../src/helpers/channels/delete_channel.ts";
|
||||
import { defaultTestOptions, tempData } from "../ws/start_bot.ts";
|
||||
import { delayUntil } from "../util/delay_until.ts";
|
||||
import { defaultTestOptions, tempData } from "../ws/start_bot.ts";
|
||||
|
||||
Deno.test({
|
||||
name: "[channel] delete a channel without a reason.",
|
||||
@@ -21,7 +21,7 @@ Deno.test({
|
||||
}
|
||||
|
||||
// Delete the channel now without a reason
|
||||
await deleteChannel(tempData.guildId, channel.id);
|
||||
await deleteChannel(channel.id);
|
||||
// wait 5 seconds to give it time for CHANNEL_DELETE event
|
||||
await delayUntil(3000, () => !cache.channels.has(channel.id));
|
||||
// Make sure it is gone from cache
|
||||
@@ -51,7 +51,7 @@ Deno.test({
|
||||
}
|
||||
|
||||
// Delete the channel now without a reason
|
||||
await deleteChannel(tempData.guildId, channel.id, "with a reason");
|
||||
await deleteChannel(channel.id, "with a reason");
|
||||
// wait 5 seconds to give it time for CHANNEL_DELETE event
|
||||
await delayUntil(10000, () => !cache.channels.has(channel.id));
|
||||
// Make sure it is gone from cache
|
||||
|
||||
@@ -15,7 +15,7 @@ async function ifItFailsBlameWolf(options: CreateGuildChannel, _save = false) {
|
||||
|
||||
// Assertions
|
||||
assertExists(channel);
|
||||
assertEquals(channel.type, options.type || DiscordChannelTypes.GUILD_TEXT);
|
||||
assertEquals(channel.type, options.type || DiscordChannelTypes.GuildText);
|
||||
|
||||
// Delay the execution by 5 seconds to allow CHANNEL_CREATE event to be processed
|
||||
await delayUntil(10000, () => cache.channels.has(channel.id));
|
||||
|
||||
@@ -16,7 +16,7 @@ async function ifItFailsBlameWolf(options: CreateGuildChannel) {
|
||||
|
||||
// Assertions
|
||||
assertExists(channel);
|
||||
assertEquals(channel.type, options.type || DiscordChannelTypes.GUILD_TEXT);
|
||||
assertEquals(channel.type, options.type || DiscordChannelTypes.GuildText);
|
||||
|
||||
// Delay the execution by 5 seconds to allow CHANNEL_CREATE event to be processed
|
||||
await delayUntil(10000, () => cache.channels.has(channel.id));
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { botId } from "../../src/bot.ts";
|
||||
import { cache } from "../../src/cache.ts";
|
||||
import { createChannel } from "../../src/helpers/channels/create_channel.ts";
|
||||
import { defaultTestOptions, tempData } from "../ws/start_bot.ts";
|
||||
import { delayUntil } from "../util/delay_until.ts";
|
||||
import { assertEquals, assertExists } from "../deps.ts";
|
||||
import { isChannelSynced } from "../../src/helpers/channels/is_channel_synced.ts";
|
||||
import { DiscordChannelTypes } from "../../src/types/channels/channel_types.ts";
|
||||
import { botId } from "../../src/bot.ts";
|
||||
import { DiscordOverwriteTypes } from "../../src/types/channels/overwrite_types.ts";
|
||||
import { bigintToSnowflake } from "../../src/util/bigint.ts";
|
||||
import { assertEquals, assertExists } from "../deps.ts";
|
||||
import { delayUntil } from "../util/delay_until.ts";
|
||||
import { defaultTestOptions, tempData } from "../ws/start_bot.ts";
|
||||
|
||||
Deno.test({
|
||||
name: "[channel] is channel synced.",
|
||||
async fn() {
|
||||
const category = await createChannel(tempData.guildId, {
|
||||
name: "synced-category",
|
||||
type: DiscordChannelTypes.GUILD_CATEGORY,
|
||||
type: DiscordChannelTypes.GuildCategory,
|
||||
permissionOverwrites: [
|
||||
{
|
||||
id: bigintToSnowflake(botId),
|
||||
|
||||
Reference in New Issue
Block a user