diff --git a/src/helpers/channels/clone_channel.ts b/src/helpers/channels/clone_channel.ts new file mode 100644 index 000000000..8c48d47ad --- /dev/null +++ b/src/helpers/channels/clone_channel.ts @@ -0,0 +1,38 @@ +import { cacheHandlers } from "../../cache.ts"; +import { createChannel } from "./create_channel.ts"; +import { Errors } from "../../types/misc/errors.ts"; +import { DiscordChannelTypes } from "../../types/channels/channel_types.ts"; +import { calculatePermissions } from "../../util/permissions.ts"; +import { Overwrite } from "../../types/channels/overwrite.ts"; + +/** Create a copy of a channel */ +export async function cloneChannel(channelId: string, reason?: string) { + const channelToClone = await cacheHandlers.get("channels", channelId); + //Return undefined if channel is not cached + if (!channelToClone) throw new Error(Errors.CHANNEL_NOT_FOUND); + + //Check for DM channel + if ( + channelToClone.type === DiscordChannelTypes.DM || + channelToClone.type === DiscordChannelTypes.GROUP_DM + ) { + throw new Error(Errors.CHANNEL_NOT_IN_GUILD); + } + + //Convert channel permission + const newOverwrites: Overwrite[] = []; + + channelToClone.permissionOverwrites.forEach((overwrite) => { + newOverwrites.push({ + id: overwrite.id, + type: overwrite.type, + allow: calculatePermissions(BigInt(overwrite.allow)), + deny: calculatePermissions(BigInt(overwrite.deny)), + }); + }); + + channelToClone.permissionOverwrites = newOverwrites; + + //Create the channel (also handles permissions) + return createChannel(channelToClone.guildId!, channelToClone, reason); +} diff --git a/src/structures/channel.ts b/src/structures/channel.ts index 71086cc4f..e3955e8c8 100644 --- a/src/structures/channel.ts +++ b/src/structures/channel.ts @@ -5,6 +5,7 @@ import { deleteChannel } from "../helpers/channels/delete_channel.ts"; import { deleteChannelOverwrite } from "../helpers/channels/delete_channel_overwrite.ts"; import { editChannel } from "../helpers/channels/edit_channel.ts"; import { editChannelOverwrite } from "../helpers/channels/edit_channel_overwrite.ts"; +import { cloneChannel } from "../helpers/channels/clone_channel.ts"; import { sendMessage } from "../helpers/messages/send_message.ts"; import { disconnectMember } from "../helpers/mod.ts"; import { Channel, DiscordChannel } from "../types/channels/channel.ts"; @@ -30,8 +31,8 @@ const baseChannel: Partial = { return `<#${this.id!}>`; }, get voiceStates() { - return this.guild?.voiceStates.filter((voiceState) => - voiceState.channelId === this.id + return this.guild?.voiceStates.filter( + (voiceState) => voiceState.channelId === this.id, ); }, get connectedMembers() { @@ -68,6 +69,9 @@ const baseChannel: Partial = { edit(options, reason) { return editChannel(this.id!, options, reason); }, + clone(reason) { + return cloneChannel(this.id!, reason); + }, }; /** Create a structure object */ @@ -125,13 +129,13 @@ export interface DiscordenoChannel mention: string; /** * Gets the voice states for this channel - * + * * ⚠️ ADVANCED: If you use the custom cache, these will not work for you. Getters can not be async and custom cache requires async. */ voiceStates?: Collection; /** * Gets the connected members for this channel undefined if member is not cached - * + * * ⚠️ ADVANCED: If you use the custom cache, these will not work for you. Getters can not be async and custom cache requires async. */ connectedMembers?: Collection; @@ -159,8 +163,7 @@ export interface DiscordenoChannel permissions: PermissionStrings[], ): ReturnType; /** Edit the channel */ - edit( - options: ModifyChannel, - reason?: string, - ): ReturnType; + edit(options: ModifyChannel, reason?: string): ReturnType; + /** Create a new channel with the same properties */ + clone(reason?: string): ReturnType; } diff --git a/tests/channels/clone_channel.ts b/tests/channels/clone_channel.ts new file mode 100644 index 000000000..41759cca6 --- /dev/null +++ b/tests/channels/clone_channel.ts @@ -0,0 +1,159 @@ +import { defaultTestOptions, tempData } from "../ws/start_bot.ts"; +import { assertEquals, assertExists } from "../deps.ts"; +import { cache } from "../../src/cache.ts"; +import { cloneChannel } from "../../src/helpers/channels/clone_channel.ts"; +import { createChannel } from "../../src/helpers/channels/create_channel.ts"; +import { CreateGuildChannel } from "../../src/types/guilds/create_guild_channel.ts"; +import { delayUntil } from "../util/delay_until.ts"; +import { botId } from "../../src/bot.ts"; +import { DiscordOverwriteTypes } from "../../src/types/channels/overwrite_types.ts"; +import { DiscordChannelTypes } from "../../src/types/channels/channel_types.ts"; + +async function ifItFailsBlameWolf(options: CreateGuildChannel, save = false) { + const channel = await createChannel(tempData.guildId, options); + + const cloned = await cloneChannel(channel.id); + + //Assertations + assertExists(cloned); + assertEquals(cloned.type, channel.type); + + // Delay the execution to allow CHANNEL_CREATE event to be processed + await delayUntil(10000, () => cache.channels.has(cloned.id)); + + if (!cache.channels.has(cloned.id)) { + throw new Error(`The channel seemed to be cloned but was not cached.`); + } + + if (channel.topic && cloned.topic !== channel.topic) { + throw new Error( + "The clone was supposed to have a topic but it does not appear to be the same topic.", + ); + } + + if (channel.bitrate && cloned.bitrate !== channel.bitrate) { + throw new Error( + "The clone was supposed to have a bitrate but it does not appear to be the same bitrate.", + ); + } + + if ( + channel.permissionOverwrites && + cloned.permissionOverwrites?.length !== channel.permissionOverwrites.length + ) { + throw new Error( + "The clone was supposed to have a permissionOverwrites but it does not appear to be the same permissionOverwrites.", + ); + } +} +Deno.test({ + name: "[channel] clone a new text channel", + async fn() { + await ifItFailsBlameWolf({ name: "Discordeno-clone-test" }, false); + }, + ...defaultTestOptions, +}); + +Deno.test({ + name: "[channel] clone a new category channel", + async fn() { + await ifItFailsBlameWolf( + { + name: "Discordeno-clone-test", + type: DiscordChannelTypes.GUILD_CATEGORY, + }, + false, + ); + }, + ...defaultTestOptions, +}); + +Deno.test({ + name: "[channel] clone a new voice channel", + async fn() { + await ifItFailsBlameWolf( + { + name: "Discordeno-clone-test", + type: DiscordChannelTypes.GUILD_VOICE, + }, + false, + ); + }, + ...defaultTestOptions, +}); + +Deno.test({ + name: "[channel] clone a new voice channel with a bitrate", + async fn() { + await ifItFailsBlameWolf( + { + name: "discordeno-clone-test", + type: DiscordChannelTypes.GUILD_VOICE, + bitrate: 32000, + }, + false, + ); + }, + ...defaultTestOptions, +}); + +Deno.test({ + name: "[channel] clone a new voice channel with a user limit", + async fn() { + await ifItFailsBlameWolf( + { + name: "Discordeno-clone-test", + type: DiscordChannelTypes.GUILD_VOICE, + userLimit: 32, + }, + false, + ); + }, + ...defaultTestOptions, +}); + +Deno.test({ + name: "[channel] clone a new text channel with a rate limit per user", + async fn() { + await ifItFailsBlameWolf( + { + name: "Discordeno-clone-test", + rateLimitPerUser: 2423, + }, + false, + ); + }, + ...defaultTestOptions, +}); + +Deno.test({ + name: "[channel] clone a new text channel with NSFW", + async fn() { + await ifItFailsBlameWolf( + { name: "Discordeno-clone-test", nsfw: true }, + false, + ); + }, + ...defaultTestOptions, +}); + +Deno.test({ + name: "[channel] clone a new text channel with permission overwrites", + async fn() { + await ifItFailsBlameWolf( + { + name: "Discordeno-clone-test", + permissionOverwrites: [ + { + id: botId, + type: DiscordOverwriteTypes.MEMBER, + allow: ["VIEW_CHANNEL"], + deny: [], + }, + ], + }, + false, + ); + }, + ...defaultTestOptions, +}); diff --git a/tests/mod.ts b/tests/mod.ts index a8d890d61..4dcd6f57e 100644 --- a/tests/mod.ts +++ b/tests/mod.ts @@ -16,6 +16,7 @@ import "./guilds/create_guild.ts"; import "./channels/category_children.ts"; import "./channels/channel_overwrite_has_permission.ts"; import "./channels/create_channel.ts"; +import "./channels/clone_channel.ts"; import "./channels/delete_channel.ts"; import "./channels/delete_channel_overwrite.ts"; import "./channels/edit_channel.ts";