diff --git a/README.md b/README.md index d9650d9..0e583e2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # Discord Temporary Voice Channels -This framework works the same way its parent does (see [discord-temp-channels](https://github.com/Androz2091/discord-temp-channels) of [Androz2091](https://github.com/Androz2091)) except that it brings a few new features: -- create a temporary text channel via a textual command -- delete a temporary text channel via a textual command +This framework works the same way its parent does (see [discord-temp-channels](https://github.com/Androz2091/discord-temp-channels) of [Androz2091](https://github.com/Androz2091) except that it brings a few new features: +- create/delete a temporary text channel via an event - more events like `voiceChannelCreate`, `textChannelCreate` and **2 more** - give the temporary channels' owner the `MANAGE_CHANNELS` permission on them - give more permissions to users/roles when channels are created (via registered parent options) @@ -18,46 +17,49 @@ npm install --save @hunteroi/discord-temp-channels ## Example -```js -const Discord = require('discord.js'); -const client = new Discord.Client(); - -const TempChannelsManager = require('@hunteroi/discord-temp-channels'); -const manager = new TempChannelsManager(client, "textchannel"); - -// Register a new main channel -manager.registerChannel('688084899537616999', { - childCategory: '569985103175090216', - childAutoDelete: true, - childFormat: (username, count) => `[DRoom #${count}] ${username}`, - childFormatRegex: /^\[DRoom #\d+\]\s+.+/i -}); - -client.login(); // discord.js will automatically load your token from process.env.DISCORD_TOKEN if set -``` +See [./example/index.js](example/index.js). ## Events ```ts -manager.on("voiceChannelCreate", (voiceChannel: VoiceChannel) => {}); +manager.on('voiceChannelCreate', (voiceChannel: VoiceChannel) => {}); -manager.on("voiceChannelDelete", (voiceChannel: VoiceChannel) => {}); +manager.on('voiceChannelDelete', (voiceChannel: VoiceChannel) => {}); -manager.on("textChannelCreate", (textChannel: TextChannel) => {}); +manager.on('textChannelCreate', (textChannel: TextChannel) => {}); -manager.on("textChannelDelete", (textChannel: TextChannel) => {}); +manager.on('textChannelDelete', (textChannel: TextChannel) => {}); -manager.on("childPrefix", (channel: GuildChannel) => {}); +manager.on('childPrefix', (channel: GuildChannel) => {}); -manager.on("childCreate", (member: GuildMember | ClientUser, child: ChildChannelData, parent: ParentChannelData) => {}); +manager.on('childCreate', (member: GuildMember | ClientUser, child: ChildChannelData, parent: ParentChannelData) => {}); -manager.on("childDelete", (member: GuildMember | ClientUser, child: ChildChannelData, parent: ParentChannelData) => {}); +manager.on('childDelete', (member: GuildMember | ClientUser, child: ChildChannelData, parent: ParentChannelData) => {}); -manager.on("channelRegister", (parent: ParentChannelData) => {}); +manager.on('channelRegister', (parent: ParentChannelData) => {}); -manager.on("channelUnregister", (parent: ParentChannelData) => {}); +manager.on('channelUnregister', (parent: ParentChannelData) => {}); -manager.on("error", (error: Error, message: string) => {}); +manager.on('error', (error: Error, message: string) => {}); ``` -## Can be improved (PR accepted) -- Creation & deletion of text channel through textual command (`./src/handlers/message`) +## Commands +In order to trigger the creation/deletion of a text channel, please emit the event `createText` using your instance of the `TempChannelsManager` class and pass it the message object: +```ts +client.on('message', message => { + if (message.content.startsWith('!createText')) { + manager.emit('createText', message); + } +}); +``` + +## Contribution +Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are greatly appreciated. + +1. Fork the Project +2. Create your Branch: `git checkout -b patch/YourAmazingWork` +3. Commit your Changes: `git commit -m 'Add some amazing work'` +4. Push to the Branch: `git push origin patch/YourAmazingWork` +5. Open a Pull Request + +## Credits +Thanks to [Androz2091](https://github.com/Androz2091) for their initial package. My package is a result of a fork of their work. Check them out! \ No newline at end of file diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000..8c5df38 --- /dev/null +++ b/example/index.js @@ -0,0 +1,27 @@ +const { Client } = require('discord.js'); +const TempChannelsManager = require('../lib'); + +const client = new Client(); +const manager = new TempChannelsManager(client); + +client.on('ready', () => { + console.log('Connected!'); + + manager.registerChannel('CHANNEL_ID', { + childCategory: 'CATEGORY_ID', + childAutoDelete: true, + childAutoDeleteIfOwnerLeaves: false, + childVoiceFormat: (str, count) => `Example #${count} | ${str}`, + childVoiceFormatRegex: /^Example #\d+ \|/, + childTextFormat: (str, count) => `example-${count}_${str}`, + childTextFormatRegex: /^example-\d+_/i, + }); +}); + +client.on('message', msg => { + if (msg.content.startsWith('!createText')) { + manager.emit('createText', msg); + } +}); + +client.login('TOKEN'); diff --git a/package-lock.json b/package-lock.json index b92be6d..7ff97c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@hunteroi/discord-temp-channels", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 593d75c..3f9fab7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hunteroi/discord-temp-channels", - "version": "1.0.2-alpha", + "version": "1.0.2", "description": "Based on Androz2091's package, this is a simple framework to facilitate the creation of a temporary voice & text channels system using Discord.js", "main": "lib/index.js", "bugs": { @@ -23,7 +23,8 @@ "build": "tsc", "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\" --tab-width 4", "lint": "tslint -p tsconfig.json", - "prepare": "npm run build" + "prepare": "npm run build", + "example": "npm run build & node ./example/index.js" }, "dependencies": { "discord.js": "^12.5.1" diff --git a/src/handlers/channelDelete.ts b/src/handlers/channelDelete.ts index 1ff9d7e..0adbd36 100644 --- a/src/handlers/channelDelete.ts +++ b/src/handlers/channelDelete.ts @@ -1,17 +1,20 @@ -import { GuildChannel, VoiceChannel } from 'discord.js'; +import { GuildChannel, VoiceChannel, Snowflake } from 'discord.js'; import TempChannelsManager from '../index'; +import { ChildChannelData } from '../types'; -export const handleChannelDelete = async (manager: TempChannelsManager, channel: GuildChannel) => { +const isVoiceOrTextChannel = (c: ChildChannelData, id: Snowflake) => c.voiceChannel.id === id || c.textChannel?.id === id; + +export const handleChannelDelete = async (manager: TempChannelsManager, channel: GuildChannel) => { let parent = manager.channels.get(channel.id); if (parent) { manager.channels.delete(channel.id); return manager.emit('channelUnregister', parent); } - parent = manager.channels.find(p => p.children.some(c => c.voiceChannel.id === channel.id || c.textChannel.id === channel.id)); + parent = manager.channels.find(p => p.children.some(c => isVoiceOrTextChannel(c, channel.id))); if (!parent) return; - const child = parent.children.find(c => c.textChannel.id === channel.id || c.voiceChannel.id === channel.id); + const child = parent.children.find(c => isVoiceOrTextChannel(c, channel.id)); if (!child) return; const textChannel = child.textChannel; diff --git a/src/handlers/channelRegister.ts b/src/handlers/channelRegister.ts index 46d0c06..82ab1cf 100644 --- a/src/handlers/channelRegister.ts +++ b/src/handlers/channelRegister.ts @@ -1,26 +1,30 @@ -import { Collection, VoiceChannel } from 'discord.js'; +import { Collection, VoiceChannel, Snowflake, GuildChannel } from 'discord.js'; import TempChannelsManager from '../index'; import { ChildChannelData, ParentChannelData } from '../types'; -export const handleRegistering = (manager: TempChannelsManager, parent: ParentChannelData) => { - const channel = manager.client.channels.resolve(parent.channelID) as VoiceChannel; +export const handleRegistering = async (manager: TempChannelsManager, parent: ParentChannelData) => { + const parentChannel = manager.client.channels.resolve(parent.channelID) as VoiceChannel; // reconstruct parent's children array when bot is ready - if (channel) { - const childrenCollection = channel.parent.children.filter(c => parent.options.childFormatRegex.test(c.name)); - const textChildren = childrenCollection.filter(c => c.isText()); - const voiceChildren = childrenCollection.filter(c => c.type === 'voice'); + if (parentChannel && parent.options.childVoiceFormatRegex) { + let textChildren = new Collection(); + const voiceChildren = parentChannel.parent.children + .filter(c => parent.options.childVoiceFormatRegex.test(c.name) && c.type === 'voice' && c.permissionOverwrites.some(po => po.type === 'member')); + if (parent.options.childTextFormatRegex) { + textChildren = parentChannel.parent.children + .filter(c => parent.options.childTextFormatRegex.test(c.name) && c.isText() && c.permissionOverwrites.some(po => po.type === 'member')); + } - parent.children = voiceChildren.map(child => { + parent.children = await Promise.all(voiceChildren.map(async child => { const ownerId = child.permissionOverwrites.find(po => po.type === 'member').id; - const owner = child.guild.members.resolve(ownerId); + const owner = await child.guild.members.fetch(ownerId); return { owner: owner, voiceChannel: child as VoiceChannel, textChannel: textChildren.find(c => c.permissionOverwrites.some(po => po.type === 'member' && po.id === ownerId)), } as ChildChannelData; - }); + })); // remove children if voice channels are empty when bot is ready parent.children = Array.from( @@ -37,10 +41,10 @@ export const handleRegistering = (manager: TempChannelsManager, parent: ParentCh await child.voiceChannel.delete(); manager.emit('voiceChannelDelete', child.voiceChannel); - manager.emit('childDelete', manager.client.user, child, manager.client.channels.cache.get(parent.channelID)); + manager.emit('childDelete', manager.client.user, child, parentChannel); } }) - .filter(c => c.voiceChannel.deleted) + .filter(c => !c.voiceChannel.deleted) .values() ); } diff --git a/src/handlers/channelUpdate.ts b/src/handlers/channelUpdate.ts index 92d8db1..c87270e 100644 --- a/src/handlers/channelUpdate.ts +++ b/src/handlers/channelUpdate.ts @@ -9,14 +9,20 @@ export const handleChannelUpdate = async (manager: TempChannelsManager, oldState if (!parent) return; const child = parent.children.find(c => c.voiceChannel.id === oldState.id || c.textChannel?.id === oldState.id); - if(!child) return; + if (!child) return; - const nameDoesNotHavePrefix = !parent.options.childFormatRegex.test(newState.name); - if (nameDoesNotHavePrefix) { + const isVoice = newState.type === 'voice'; + const nameDoesNotHavePrefix = isVoice + ? !parent.options.childVoiceFormatRegex.test(newState.name) + : !parent.options.childTextFormatRegex.test(newState.name); + + if (nameDoesNotHavePrefix) { const count = parent.children.indexOf(child) + 1; - const name = parent.options.childFormat(newState.name, count); + const name = isVoice + ? parent.options.childVoiceFormat(newState.name, count) + : parent.options.childTextFormat(newState.name, count); newState.setName(name); - manager.emit("childPrefix", newState); + manager.emit('childPrefix', newState); } }; diff --git a/src/handlers/message.ts b/src/handlers/createText.ts similarity index 82% rename from src/handlers/message.ts rename to src/handlers/createText.ts index fef1f78..956a0b5 100644 --- a/src/handlers/message.ts +++ b/src/handlers/createText.ts @@ -1,11 +1,11 @@ import { Message } from 'discord.js'; import TempChannelsManager from '..'; -export const handleMessage = async (manager: TempChannelsManager, commandName: string, message: Message) => { +export const handleTextCreation = async (manager: TempChannelsManager, message: Message) => { + if (!message) return; + const owner = message.member; - - if (!message.content.includes(commandName)) return; - + const voiceChannel = message.member.voice.channel; if (!voiceChannel || !manager.channels.some(p => p.children.some(c => c.voiceChannel.id === voiceChannel.id))) return; const parent = manager.channels.find(p => p.children.some(c => c.voiceChannel.id === voiceChannel.id)); @@ -16,7 +16,7 @@ export const handleMessage = async (manager: TempChannelsManager, commandName: s if (!child.textChannel) { const count = parent.children.indexOf(child) + 1; - const newChannelName = parent.options.childFormat(owner.displayName, count); + const newChannelName = parent.options.childTextFormat(owner.displayName, count); const textChannel = await message.guild.channels.create(newChannelName, { parent: parent.options.childCategory, diff --git a/src/handlers/index.ts b/src/handlers/index.ts index d960060..401e266 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -1,5 +1,5 @@ export * from './channelDelete'; export * from './channelRegister'; export * from './channelUpdate'; -export * from './message'; +export * from './createText'; export * from './voiceStateUpdate'; diff --git a/src/handlers/voiceStateUpdate.ts b/src/handlers/voiceStateUpdate.ts index 9a3127d..4c152f8 100644 --- a/src/handlers/voiceStateUpdate.ts +++ b/src/handlers/voiceStateUpdate.ts @@ -26,7 +26,7 @@ export const handleVoiceStateUpdate = async (manager: TempChannelsManager, oldSt await child.voiceChannel.delete(); manager.emit('voiceChannelDelete', child.voiceChannel); - parent.children = parent.children.filter(c => c.voiceChannel.id !== c.voiceChannel.id); + parent.children = parent.children.filter(c => c.voiceChannel.id !== child.voiceChannel.id); manager.emit('childDelete', newState.member, child, manager.client.channels.cache.get(parent.channelID)); } catch (err) { manager.emit('error', err, 'Cannot auto delete channel ' + child.voiceChannel.id); @@ -39,7 +39,7 @@ export const handleVoiceStateUpdate = async (manager: TempChannelsManager, oldSt if (!parent) return; const count = parent.children.length + 1; - const newChannelName = parent.options.childFormat(newState.member.displayName, count); + const newChannelName = parent.options.childVoiceFormat(newState.member.displayName, count); const voiceChannel = await newState.guild.channels.create(newChannelName, { parent: parent.options.childCategory, bitrate: parent.options.childBitrate, diff --git a/src/index.ts b/src/index.ts index 7f89579..8d6bf00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,8 +6,8 @@ import { handleChannelDelete, handleVoiceStateUpdate, handleChannelUpdate, - handleMessage, - handleRegistering + handleTextCreation, + handleRegistering, } from './handlers'; /** @@ -16,7 +16,7 @@ import { * @class TempChannelsManager * @extends {EventEmitter} */ -export default class TempChannelsManager extends EventEmitter { +class TempChannelsManager extends EventEmitter { /** *The client instance. * @type {Client} @@ -34,21 +34,20 @@ export default class TempChannelsManager extends EventEmitter { /** *Creates an instance of TempChannelsManager. * @param {Client} client - * @param {string} commandName * @memberof TempChannelsManager */ - constructor(client: Client, commandName: string) { + constructor(client: Client) { super(); this.channels = new Collection(); this.client = client; - this.client.on('message', async (message) => handleMessage(this, commandName, message)); this.client.on('voiceStateUpdate', async (oldState, newState) => handleVoiceStateUpdate(this, oldState, newState)); this.client.on('channelUpdate', async (oldState, newState) => handleChannelUpdate(this, oldState as GuildChannel, newState as GuildChannel)); this.client.on('channelDelete', async channel => handleChannelDelete(this, channel as GuildChannel)); - this.on('channelRegister', (parent) => handleRegistering(this, parent)); + this.on('channelRegister', async (parent) => handleRegistering(this, parent)); + this.on('createText', async (message) => handleTextCreation(this, message)); } /** @@ -71,8 +70,10 @@ export default class TempChannelsManager extends EventEmitter { childCategory: null, childAutoDelete: true, childAutoDeleteIfOwnerLeaves: false, - childFormat: (name, count) => `[DRoom #${count}] ${name}`, - childFormatRegex: /^\[DRoom #\d+\]\s+.+/i, + childVoiceFormat: (name, count) => `[DRoom #${count}] ${name}`, + childVoiceFormatRegex: /^\[DRoom #\d+\]\s+.+/i, + childTextFormat: (name, count) => `droom-${count}_${name}`, + childTextFormatRegex: /^droom-\d+_/i, childPermissionOverwriteOption: { MANAGE_CHANNELS: true } } ) { @@ -98,3 +99,5 @@ export default class TempChannelsManager extends EventEmitter { return this.emit('error', null, `There is no channel with the id ${channelID}`); } } + +export = TempChannelsManager; diff --git a/src/types/ParentChannelOptions.ts b/src/types/ParentChannelOptions.ts index 5d7a794..f371f57 100644 --- a/src/types/ParentChannelOptions.ts +++ b/src/types/ParentChannelOptions.ts @@ -11,8 +11,10 @@ export interface ParentChannelOptions { childCategory?: Snowflake; childAutoDelete: boolean; childAutoDeleteIfOwnerLeaves: boolean; - childFormat(str: string, count: number): string; - childFormatRegex: RegExp; + childVoiceFormat(str: string, count: number): string; + childVoiceFormatRegex: RegExp; + childTextFormat(str: string, count: number): string; + childTextFormatRegex: RegExp; childMaxUsers?: number; childBitrate?: number; childPermissionOverwriteOption?: PermissionOverwriteOption;