Skip to content

Commit

Permalink
Add support for threads and an auto reply message (#3)
Browse files Browse the repository at this point in the history
* feat: allow to create a thread per ticket

* feat: allow to send an auto reply to the ticket author

* refactor: define the new events correctly

* docs: reference the 2 new events

* docs: remove the todo list

* refactor: remove unused parameter & add checks for thread
  • Loading branch information
HunteRoi authored Oct 17, 2021
1 parent 30b13a9 commit 4326e79
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 65 deletions.
37 changes: 21 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

Discord Mailbox is a framework to easily add a mailbox inside your bot. The feature is also fully customizable.

- Supports multiple tickets per users
- Logs everything like you want it
- Emits events like `ticketCreate`, `ticketUpdate` and **6 more**
- Allow full customization of the embed (you can add image, thumbnail, etc)
- And much more!
- Supports multiple tickets per users
- Logs everything like you want it
- Emits events like `ticketCreate`, `ticketUpdate` and **8 more**
- Allow full customization of the embed (you can add image, thumbnail, etc)
- And much more!

![IMAGE](./assets/example.gif)

Expand All @@ -20,12 +20,12 @@ You also must not forget to include [mandatory intents and partials](#mandatory-

### Mandatory intents and partials

- GUILDS: used to access guild content such as channels.
- GUILD_MESSAGES: used to read guild messages.
- GUILD_MESSAGE_REACTIONS: used to access guild messages reactions.
- DIRECT_MESSAGES: used to access direct messages to the bot.
- CHANNEL: used to receive events when the bot is DMed.
- MESSAGE: used to read the messages even if incomplete.
- GUILDS: used to access guild content such as channels.
- GUILD_MESSAGES: used to read guild messages.
- GUILD_MESSAGE_REACTIONS: used to access guild messages reactions.
- DIRECT_MESSAGES: used to access direct messages to the bot.
- CHANNEL: used to receive events when the bot is DMed.
- MESSAGE: used to read the messages even if incomplete.

## Installation

Expand Down Expand Up @@ -61,6 +61,16 @@ manager.on(
);

manager.on(MailboxManagerEvents.replyDelete, (message: Message) => {});

manager.on(
MailboxManagerEvents.threadCreate,
(ticket: Ticket, thread: ThreadChannel) => {}
);

manager.on(
MailboxManagerEvents.threadArchive,
(ticket: Ticket, thread: ThreadChannel) => {}
);
```

## Contribution
Expand All @@ -72,8 +82,3 @@ Contributions are what make the open source community such an amazing place to b
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

## Todo

- auto reply when a ticket is opened
- support thread
10 changes: 7 additions & 3 deletions example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ const manager = new MailboxManager(client, {
msg.cleanContent
}`,
generateMessage: (ticket) =>
`Logs for ticket ${ticket.id} - closed at ${new Date(
ticket.closedAt
)}`,
`Logs for ticket ${ticket.id} - closed at ${new Date(ticket.closedAt)}`,
sendToRecipient: false,
channel: 'TEXT_CHANNEL_ID',
showName: false,
},
mailboxChannel: 'TEXT_CHANNEL_ID',
threadOptions: {
name: (ticket) => `Ticket ${ticket.id}`,
startMessage: (ticket) => `New ticket created for <@${ticket.createdBy}>`,
},
deleteReplies: true,
cronTime: '* * * * *', // run each minute
closeTicketAfter: 60, // in seconds
Expand All @@ -37,6 +39,8 @@ const manager = new MailboxManager(client, {
'You are not allowed to mention @everyone or @here in a mail!',
replyMessage:
'Please use the "reply" feature to send an answer to this message.',
autoReplyMessage:
'Your ticket has been received and will be treated soon. Please remain patient as we get back to you!',
tooMuchTickets:
'You have too much tickets that are not closed! Please wait for your tickets to be closed before submitting new ones.',
ticketClose: (nbTickets) =>
Expand Down
49 changes: 35 additions & 14 deletions src/MailboxManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
handleReaction,
handleClosing,
handleLog,
handleOpening,
} from './handlers';
import { MailboxManagerOptions, Ticket } from './types';
import { MailboxManagerEvents } from '.';
Expand Down Expand Up @@ -70,8 +71,7 @@ export class MailboxManager extends EventEmitter {
client: Client,
options: MailboxManagerOptions = {
tooMuchTickets: 'You have too much opened tickets!',
notAllowedToPing:
'You are not allowed to mention @everyone and @here.',
notAllowedToPing: 'You are not allowed to mention @everyone and @here.',
replyMessage: 'Use the "reply" feature to respond.',
ticketClose: (nbUserTicketsLeft) =>
`This ticket has been closed. You now have ${nbUserTicketsLeft} tickets that are still opened.`,
Expand All @@ -80,6 +80,7 @@ export class MailboxManager extends EventEmitter {
formatTitle: (id) => `Ticket ${id}`,
cronTime: '* * * * *',
mailboxChannel: null,
threadOptions: null,
}
) {
super();
Expand All @@ -89,9 +90,7 @@ export class MailboxManager extends EventEmitter {
throw new Error('GUILDS intent is required to use this package!');
}
if (!intents.has(Intents.FLAGS.GUILD_MESSAGES)) {
throw new Error(
'GUILD_MESSAGES intent is required to use this package!'
);
throw new Error('GUILD_MESSAGES intent is required to use this package!');
}
if (!intents.has(Intents.FLAGS.DIRECT_MESSAGES)) {
throw new Error(
Expand All @@ -116,9 +115,7 @@ export class MailboxManager extends EventEmitter {
}

if (!options.mailboxChannel) {
throw new Error(
'Please define the mailbox channel in the options!'
);
throw new Error('Please define the mailbox channel in the options!');
}

this.client = client;
Expand All @@ -131,14 +128,14 @@ export class MailboxManager extends EventEmitter {
handleMessage(this, message);
});
if (this.options.forceCloseEmoji) {
this.client.on(
'messageReactionAdd',
async (messageReaction, user) => {
await handleReaction(this, messageReaction, user);
}
);
this.client.on('messageReactionAdd', async (messageReaction, user) => {
await handleReaction(this, messageReaction, user);
});
}

this.on(MailboxManagerEvents.ticketCreate, async (ticket: Ticket) =>
handleOpening(this, ticket)
);
this.on(MailboxManagerEvents.ticketClose, async (ticket: Ticket) =>
handleClosing(this, ticket)
);
Expand Down Expand Up @@ -250,3 +247,27 @@ export class MailboxManager extends EventEmitter {
* console.log(message.id);
* });
*/

/**
* Emitted when the thread channel is created (due to the ticket being created).
* @event MailboxManager#threadCreate
* @param {Ticket} ticket The ticket
* @param {Discord.ThreadChannel} thread The thread channel
* @example
* manager.on(MailboxManagerEvents.threadCreate, (ticket, thread) => {
* console.log(ticket.id) ;
* console.log(thread.id);
* });
*/

/**
* Emitted when the thread channel is archived (due to the ticket being closed).
* @event MailboxManager#threadArchive
* @param {Ticket} ticket The ticket
* @param {Discord.ThreadChannel} thread The thread channel
* @example
* manager.on(MailboxManagerEvents.threadArchive, (ticket, thread) => {
* console.log(ticket.id) ;
* console.log(thread.id);
* });
*/
3 changes: 3 additions & 0 deletions src/MailboxManagerEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export enum MailboxManagerEvents {
ticketForceClose = 'ticketForceClose',
ticketDelete = 'ticketDelete',

threadCreate = 'threadCreate',
threadArchive = 'threadArchive',

replySent = 'replySent',
replyDelete = 'replyDelete',

Expand Down
1 change: 1 addition & 0 deletions src/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './messageCreate';
export * from './messageReactionAdd';
export * from './ticketCreate';
export * from './ticketClose';
export * from './ticketLog';
72 changes: 48 additions & 24 deletions src/handlers/messageCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
Constants,
MessageEmbed,
PartialMessage,
ThreadChannel,
AllowedThreadTypeForTextChannel,
} from 'discord.js';

import { Ticket } from '../types';
Expand All @@ -29,34 +31,45 @@ export const handleMessage = async (
}

const isFromDM =
message.channel.type ===
Constants.ChannelTypes[Constants.ChannelTypes.DM];
message.channel.type === Constants.ChannelTypes[Constants.ChannelTypes.DM];
let userTickets: Collection<string, Ticket>;
let ticket: Ticket;
let botMessage: Message;
let previousMessage: Message;
let answer: Message;

const isExistingTicket = !!message.reference;
const mainChannel = await message.client.channels.fetch(
manager.options.mailboxChannel
);
if (!mainChannel) return;
const mailboxChannel: TextChannel = mainChannel as TextChannel;
let threadChannel: ThreadChannel | null = null;

const isExistingTicket = Boolean(message.reference);
if (isExistingTicket) {
let messageId: Snowflake;

botMessage = await message.channel.messages.fetch(
previousMessage = await message.channel.messages.fetch(
message.reference.messageId
);

messageId = extractMessageId(
botMessage,
previousMessage,
!!manager.options.embedOptions
);
if (!messageId) return;

userTickets = manager.userTickets.find((userTickets) =>
userTickets.some((t) => t.messages.last().id === messageId)
userTickets.some((t: Ticket) => t.messages.last().id === messageId)
);
if (!userTickets || userTickets.size === 0) return;

ticket = userTickets.find((t) => t.messages.last().id === messageId);
ticket = userTickets.find(
(t: Ticket) => t.messages.last().id === messageId
);
if (!ticket) return;
if (ticket.threadId) {
threadChannel = await mailboxChannel.threads.fetch(ticket.threadId);
}

if (isFromDM && ticket.isOutdated()) {
return manager.emit(MailboxManagerEvents.ticketClose, ticket);
Expand Down Expand Up @@ -87,15 +100,28 @@ export const handleMessage = async (
manager.userTickets.get(ticket.createdBy).set(ticket.id, ticket);
manager.emit(MailboxManagerEvents.ticketCreate, ticket);
}

if (manager.options.threadOptions) {
const startMessage = await mailboxChannel.send(
manager.options.threadOptions.startMessage(ticket)
);
threadChannel = await mailboxChannel.threads.create({
name: manager.options.threadOptions.name(ticket),
autoArchiveDuration: 'MAX',
startMessage,
type: Constants.ChannelTypes[
Constants.ChannelTypes.GUILD_PUBLIC_THREAD
] as AllowedThreadTypeForTextChannel,
});
ticket.setThreadId(threadChannel.id);
manager.emit(MailboxManagerEvents.threadCreate, ticket, threadChannel);
}
}

const ticketMessage = ticket.generateMessage(manager, message);
switch (message.channel.type) {
case Constants.ChannelTypes[Constants.ChannelTypes.DM]: {
const mailboxChannel = await message.client.channels.fetch(
manager.options.mailboxChannel
);
answer = await (mailboxChannel as TextChannel).send(
answer = await (threadChannel ?? mailboxChannel).send(
ticketMessage instanceof MessageEmbed
? { embeds: [ticketMessage] }
: ticketMessage
Expand All @@ -107,20 +133,22 @@ export const handleMessage = async (
return;
}

case Constants.ChannelTypes[Constants.ChannelTypes.GUILD_TEXT]: {
if (botMessage) {
const embed = botMessage.embeds[0];
case Constants.ChannelTypes[Constants.ChannelTypes.GUILD_TEXT]:
case Constants.ChannelTypes[Constants.ChannelTypes.GUILD_PRIVATE_THREAD]:
case Constants.ChannelTypes[Constants.ChannelTypes.GUILD_PUBLIC_THREAD]: {
if (previousMessage) {
const embed = previousMessage.embeds[0];
if (embed) {
embed.setAuthor(embed.author.name, arrowUp);
await botMessage.edit({
content: botMessage.content || null,
await previousMessage.edit({
content: previousMessage.content || null,
embeds: [embed],
});
}

if (manager.options.replySentEmoji) {
await botMessage.reactions.removeAll();
await botMessage.react(manager.options.replySentEmoji);
await previousMessage.reactions.removeAll();
await previousMessage.react(manager.options.replySentEmoji);
}
}

Expand All @@ -136,11 +164,7 @@ export const handleMessage = async (
? { embeds: [ticketMessage] }
: ticketMessage
);
return manager.emit(
MailboxManagerEvents.replySent,
message,
answer
);
return manager.emit(MailboxManagerEvents.replySent, message, answer);
}

default:
Expand Down
20 changes: 14 additions & 6 deletions src/handlers/ticketClose.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MessageEmbed } from 'discord.js';
import { MessageEmbed, ThreadChannel } from 'discord.js';

import { MailboxManager, MailboxManagerEvents } from '..';
import { Ticket } from '../types';
Expand All @@ -11,19 +11,27 @@ export const handleClosing = async (

const userTickets = manager.userTickets.get(ticket.createdBy);
if (userTickets) {
const channel = ticket.threadId
? await manager.client.channels.fetch(ticket.threadId)
: null;
if (channel) {
const threadChannel = channel as ThreadChannel;
if (threadChannel) {
threadChannel.setArchived(true);
manager.emit(MailboxManagerEvents.threadArchive, ticket, threadChannel);
}
}

userTickets.delete(ticket.id);
if (userTickets.size === 0)
manager.userTickets.delete(ticket.createdBy);
if (userTickets.size === 0) manager.userTickets.delete(ticket.createdBy);

if (manager.options.ticketClose) {
const user = await manager.client.users.fetch(ticket.createdBy);

const text = manager.options.ticketClose(userTickets?.size ?? 0);
const message = ticket.generateMessage(manager, text);
user.send(
message instanceof MessageEmbed
? { embeds: [message] }
: message
message instanceof MessageEmbed ? { embeds: [message] } : message
);
}

Expand Down
Loading

0 comments on commit 4326e79

Please sign in to comment.