From 1a1ef34a96827c4f18865259677704e29b4096b4 Mon Sep 17 00:00:00 2001 From: Exists <55012346+existentiality@users.noreply.github.com> Date: Wed, 14 Apr 2021 04:01:35 -0400 Subject: [PATCH] Add channel cloning Changes: - Added function cloneChannel - Modified createChannel to support cloneChannel - Added clone(reason) method on channel structure - Added channel cloning tests --- src/helpers/channels/clone_channel.ts | 28 +++++++++++++++ src/helpers/channels/create_channel.ts | 26 +++++++++----- src/structures/channel.ts | 37 +++++++++++--------- tests/channels/clone_channel.ts | 48 ++++++++++++++++++++++++++ tests/mod.ts | 1 + 5 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 src/helpers/channels/clone_channel.ts create mode 100644 tests/channels/clone_channel.ts diff --git a/src/helpers/channels/clone_channel.ts b/src/helpers/channels/clone_channel.ts new file mode 100644 index 000000000..56509a036 --- /dev/null +++ b/src/helpers/channels/clone_channel.ts @@ -0,0 +1,28 @@ +import { cacheHandlers } from "../../cache.ts"; +import { createChannel } from "./create_channel.ts"; +import { CreateGuildChannel } from "../../types/guilds/create_guild_channel.ts"; +import { DiscordenoChannel } from "../../structures/channel.ts"; + +/** Create a copy of a channel */ +export async function cloneChannel(channelId: string, reason?: string) { + const channelToClone: DiscordenoChannel | undefined = await cacheHandlers.get( + "channels", + channelId + ); + //Return undefined if channel is not cached (unsure about error handling) + if (!channelToClone) return; + + //If "name" is null or undefined as specified by types + channelToClone.name ??= "new-channel"; + + //Merge channel data with reason for createChannel options + const creationData = { + reason, + ...channelToClone, + }; + //Create the channel (also handles permissions) + return createChannel( + channelToClone.guildId!, + creationData as CreateGuildChannel + ); +} diff --git a/src/helpers/channels/create_channel.ts b/src/helpers/channels/create_channel.ts index ac586c930..180028d68 100644 --- a/src/helpers/channels/create_channel.ts +++ b/src/helpers/channels/create_channel.ts @@ -17,14 +17,22 @@ import { calculateBits } from "../../util/permissions.ts"; /** Create a channel in your server. Bot needs MANAGE_CHANNEL permissions in the server. */ export async function createChannel( guildId: string, - options?: CreateGuildChannel, + options?: CreateGuildChannel ) { const requiredPerms: Set = new Set(["MANAGE_CHANNELS"]); + //Integration with permissions for channel cloning + let useDefaultOverwrites = false; + options?.permissionOverwrites?.forEach((overwrite) => { + if (overwrite.id && parseInt(overwrite.allow)) { + useDefaultOverwrites = true; + + return; + } eventHandlers.debug?.( "loop", - `Running forEach loop in create_channel file.`, + `Running forEach loop in create_channel file.` ); overwrite.allow.forEach(requiredPerms.add, requiredPerms); overwrite.deny.forEach(requiredPerms.add, requiredPerms); @@ -40,14 +48,16 @@ export async function createChannel( endpoints.GUILD_CHANNELS(guildId), { ...camelKeysToSnakeCase(options ?? {}), - permission_overwrites: options?.permissionOverwrites?.map((perm) => ({ - ...perm, + permission_overwrites: useDefaultOverwrites + ? options?.permissionOverwrites + : options?.permissionOverwrites?.map((perm) => ({ + ...perm, - allow: calculateBits(perm.allow), - deny: calculateBits(perm.deny), - })), + allow: calculateBits(perm.allow), + deny: calculateBits(perm.deny), + })), type: options?.type || DiscordChannelTypes.GUILD_TEXT, - }, + } )) as DiscordChannel; const discordenoChannel = await structures.createDiscordenoChannel(result); diff --git a/src/structures/channel.ts b/src/structures/channel.ts index 71086cc4f..88b0b9dfe 100644 --- a/src/structures/channel.ts +++ b/src/structures/channel.ts @@ -5,6 +5,8 @@ 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 { createChannel } from "../helpers/channels/create_channel.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 +32,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() { @@ -39,7 +41,7 @@ const baseChannel: Partial = { if (!voiceStates) return undefined; return new Collection( - voiceStates.map((vs, key) => [key, cache.members.get(key)]), + voiceStates.map((vs, key) => [key, cache.members.get(key)]) ); }, send(content) { @@ -62,19 +64,23 @@ const baseChannel: Partial = { this.guildId!, this.id!, overwrites, - permissions, + permissions ); }, edit(options, reason) { return editChannel(this.id!, options, reason); }, + + clone(reason) { + return cloneChannel(this.id!, reason); + }, }; /** Create a structure object */ // deno-lint-ignore require-await export async function createDiscordenoChannel( data: DiscordChannel, - guildId?: string, + guildId?: string ) { const { guildId: rawGuildId = "", @@ -86,7 +92,7 @@ export async function createDiscordenoChannel( Object.keys(rest).forEach((key) => { eventHandlers.debug?.( "loop", - `Running forEach loop in createDiscordenoChannel function.`, + `Running forEach loop in createDiscordenoChannel function.` ); // @ts-ignore index signature props[key] = createNewProp(rest[key]); @@ -96,7 +102,7 @@ export async function createDiscordenoChannel( ...props, guildId: createNewProp(guildId || rawGuildId), lastPinTimestamp: createNewProp( - lastPinTimestamp ? Date.parse(lastPinTimestamp) : undefined, + lastPinTimestamp ? Date.parse(lastPinTimestamp) : undefined ), }); @@ -125,13 +131,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; @@ -147,20 +153,19 @@ export interface DiscordenoChannel /** Edit a channel Overwrite */ editOverwrite( overwriteID: string, - options: Omit, + options: Omit ): ReturnType; /** Delete a channel Overwrite */ deleteOverwrite( - overwriteID: string, + overwriteID: string ): ReturnType; /** Checks if a channel overwrite for a user id or a role id has permission in this channel */ hasPermission( overwrites: DiscordOverwrite[], - permissions: PermissionStrings[], + 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..a2ca9d2ad --- /dev/null +++ b/tests/channels/clone_channel.ts @@ -0,0 +1,48 @@ +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 { delayUntil } from "../util/delay_until.ts"; + +Deno.test({ + name: "[channel] clone a channel", + async fn() { + const cloned = await cloneChannel(tempData.channelId, "testing"); + + //Get channel that was cloned + const originalChannel = cache.channels.get(tempData.channelId); + + //Assertation + assertExists(cloned); + assertEquals(cloned.type, originalChannel.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 (originalChannel.topic && cloned.topic !== originalChannel.topic) { + throw new Error( + "The clone was supposed to have a topic but it does not appear to be the same topic." + ); + } + + if (originalChannel.bitrate && cloned.bitrate !== originalChannel.bitrate) { + throw new Error( + "The clone was supposed to have a bitrate but it does not appear to be the same bitrate." + ); + } + + if ( + originalChannel.permissionOverwrites && + cloned.permissionOverwrites?.length !== + originalChannel.permissionOverwrites.length + ) { + throw new Error( + "The clone was supposed to have a permissionOverwrites but it does not appear to be the same permissionOverwrites." + ); + } + }, +}); diff --git a/tests/mod.ts b/tests/mod.ts index daa91c90f..f66c18bb1 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";