Skip to content

Commit

Permalink
Properly handle channel creation, deletion, recovering, Bump version
Browse files Browse the repository at this point in the history
  • Loading branch information
HunteRoi committed Jan 6, 2021
1 parent a0ca0db commit 3d50f2e
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 75 deletions.
66 changes: 34 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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!
27 changes: 27 additions & 0 deletions example/index.js
Original file line number Diff line number Diff line change
@@ -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');
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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"
Expand Down
11 changes: 7 additions & 4 deletions src/handlers/channelDelete.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
28 changes: 16 additions & 12 deletions src/handlers/channelRegister.ts
Original file line number Diff line number Diff line change
@@ -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<Snowflake, GuildChannel>();
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(
Expand All @@ -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()
);
}
Expand Down
16 changes: 11 additions & 5 deletions src/handlers/channelUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
10 changes: 5 additions & 5 deletions src/handlers/message.ts → src/handlers/createText.ts
Original file line number Diff line number Diff line change
@@ -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));
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './channelDelete';
export * from './channelRegister';
export * from './channelUpdate';
export * from './message';
export * from './createText';
export * from './voiceStateUpdate';
4 changes: 2 additions & 2 deletions src/handlers/voiceStateUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down
21 changes: 12 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
handleChannelDelete,
handleVoiceStateUpdate,
handleChannelUpdate,
handleMessage,
handleRegistering
handleTextCreation,
handleRegistering,
} from './handlers';

/**
Expand All @@ -16,7 +16,7 @@ import {
* @class TempChannelsManager
* @extends {EventEmitter}
*/
export default class TempChannelsManager extends EventEmitter {
class TempChannelsManager extends EventEmitter {
/**
*The client instance.
* @type {Client}
Expand All @@ -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));
}

/**
Expand All @@ -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 }
}
) {
Expand All @@ -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;
6 changes: 4 additions & 2 deletions src/types/ParentChannelOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 3d50f2e

Please sign in to comment.