this is a rewrite of the bot in typescript. detritus is used as a discord library instead of discord.js
This commit is contained in:
spiral 2022-10-21 17:22:01 +00:00
parent 6688d4dcd8
commit 56091a6df7
No known key found for this signature in database
GPG key ID: 244A11E4B0BCF40E
33 changed files with 731 additions and 1116 deletions

View file

@ -1,39 +0,0 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Discord = require('discord.js');
const CommandCategory = require('../model/command-category');
const Guild = require('../model/guild');
module.exports = {
aliases: ['av'],
category: CommandCategory.FUN,
isAllowedForContext: () => true,
description: 'Displays the avatar of the specified member.',
process: async (message, args) => {
let user = null;
if (args.length > 0) {
const result = Guild.findDesignatedMemberInMessage(message);
if (result.foundMembers.length > 0) {
if (result.foundMembers[0].user !== undefined) {
user = result.foundMembers[0].user;
} else {
user = result.foundMembers[0];
}
}
} else {
user = message.author;
}
if (user !== null) {
const url = user.displayAvatarURL({ dynamic: true });
message.channel.send(new Discord.MessageAttachment(
url + '?size=2048',
user.id + url.substr(url.lastIndexOf('.'))
)).catch(error => Logger.warning(error.toString()));
} else {
message.reply('I... Have no idea who that could be, sorry.');
}
}
}

25
command/avatar.ts Normal file
View file

@ -0,0 +1,25 @@
import { APIGuildMember, APIUser, GatewayMessageCreateDispatchData } from "discord-api-types/v10";
import { restClient } from "../bot";
import CommandCategory from "../model/command-category";
export default {
aliases: ['avatar', 'av'],
category: CommandCategory.FUN,
isAllowedForContext: (_: GatewayMessageCreateDispatchData) => true,
description: 'Displays the avatar of the specified member.',
process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
let user: APIUser;
if (message.mentions.length == 0)
user = message.author;
else if (message.mentions.length > 1)
return restClient.createMessage(message.channel_id, `<@${message.author.id}>, there are too many mentions in this message! `
+`please pick only one user whose avatar you want to show.`);
else user = message.mentions[0];
const url = `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=2048`;
await restClient.createMessage(message.channel_id, { embed: { image: { url } } });
}
}

View file

@ -1,66 +0,0 @@
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../config.json');
const CommandCategory = require('../model/command-category');
const CommandPermission = require('../model/command-permission');
const { search } = require('../model/jira');
const MAX_CHARACTERS = 1950;
module.exports = {
aliases: ['change-log', 'cl'],
category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberMod,
description: 'Builds the changelog for a given version',
process: async (message, args) => {
const errorHandler = async (error) => {
if (error) {
Logger.exception(error);
}
await message.reactions.removeAll();
await message.react('❌');
}
await message.react('⏳').catch(() => {});
const issues = args.length > 0 && args[0] ? await search(args[0]).catch(errorHandler) : await search().catch(errorHandler);
const taskType = Config.jira.issueTypes.task;
const bugType = Config.jira.issueTypes.bug;
const features = issues.filter(issue => parseInt(issue.fields.issuetype.id) === taskType);
const bugs = issues.filter(issue => parseInt(issue.fields.issuetype.id) === bugType);
if (features.length < 1 && bugs.length < 1) {
await message.channel.send('No issues found.');
await message.reactions.removeAll().catch(() => {});
return;
}
const output = `${features.map(
issue => `* Feature: ${issue.key} - ${issue.fields.summary}`
).join('\n')}\n\n${bugs.map(
issue => `* Fixed: ${issue.key} - ${issue.fields.summary}`
).join('\n')}`.trim();
const messages = [];
let currentMessage = '```';
for (let line of output.split('\n')) {
if (currentMessage.length + line.length >= MAX_CHARACTERS) {
messages.push(`${currentMessage}\`\`\``);
currentMessage = '```';
}
currentMessage = `${currentMessage}\n${line}`;
}
messages.push(`${currentMessage}\`\`\``);
for (let messageToSend of messages) {
await message.channel.send(messageToSend).catch(() => {});
}
await message.reactions.removeAll().catch(() => {});
await message.react('✔').catch(() => {});
}
}

67
command/changelog.ts Normal file
View file

@ -0,0 +1,67 @@
import { GatewayMessageCreateDispatchData } from 'discord-api-types/v10';
import CommandCategory from '../model/command-category';
import * as CommandPermission from '../model/command-permission';
import config from '../config';
import { restClient } from '../bot';
import search from '../model/jira';
const MAX_CHARACTERS = 1950;
export default {
aliases: ['changelog', 'change-log', 'cl'],
category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberMod,
description: 'Builds the changelog for a given version',
process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
const errorHandler = async (error: any) => {
await restClient.deleteReactions(message.channel_id, message.id);
await restClient.createReaction(message.channel_id, message.id, '❌');
throw error;
}
await restClient.createReaction(message.channel_id, message.id, '⏳').catch(() => {});
const issues = args.length > 0 && args[0] ? await search(args[0]).catch(errorHandler) : await search().catch(errorHandler);
const taskType = config.jira.issueTypes.task;
const bugType = config.jira.issueTypes.bug;
const features = issues?.filter(issue => parseInt(issue.fields.issuetype.id) === taskType) ?? [];
const bugs = issues?.filter(issue => parseInt(issue.fields.issuetype.id) === bugType) ?? [];
if (features.length < 1 && bugs.length < 1) {
await restClient.createMessage(message.channel_id, 'No issues found.');
await restClient.deleteReactions(message.channel_id, message.id);
return;
}
const output = `${features.map(
issue => `* Feature: ${issue.key} - ${issue.fields.summary}`
).join('\n')}\n\n${bugs.map(
issue => `* Fixed: ${issue.key} - ${issue.fields.summary}`
).join('\n')}`.trim();
const messages: string[] = [];
let currentMessage = '```';
for (let line of output.split('\n')) {
if (currentMessage.length + line.length >= MAX_CHARACTERS) {
messages.push(`${currentMessage}\`\`\``);
currentMessage = '```';
}
currentMessage = `${currentMessage}\n${line}`;
}
messages.push(`${currentMessage}\`\`\``);
for (let messageToSend of messages) {
await restClient.createMessage(message.channel_id, messageToSend);
}
await restClient.deleteReactions(message.channel_id, message.id).catch(() => {});
await restClient.createReaction(message.channel_id, message.id, '✔');
}
}

View file

@ -1,17 +0,0 @@
const Config = require('../config.json');
const CommandCategory = require('../model/command-category');
const CommandPermission = require('../model/command-permission');
module.exports = {
aliases: ['clear', 'purge'],
category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberMod,
description: 'Deletes messages in bulk.',
process: async (message, args) => {
if (args.length > 0 && parseInt(args[0]) > 0) {
await message.channel.bulkDelete(Math.min(parseInt(args[0]) + 1, 100));
} else {
message.reply(`You have to tell me how many messages I should clean. \`${Config.prefix}clean 10\` for example.`);
}
}
}

25
command/clean.ts Normal file
View file

@ -0,0 +1,25 @@
import { APIMessage, GatewayMessageCreateDispatchData } from 'discord-api-types/v10';
import CommandCategory from '../model/command-category';
import * as CommandPermission from '../model/command-permission';
import config from '../config';
import { restClient } from '../bot';
export default {
aliases: ['clean', 'clear', 'purge'],
category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberMod,
description: 'Deletes messages in bulk.',
process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
if (args.length > 0 && parseInt(args[0]) > 0) {
// sometimes abstractions are nice...
// await message.channel.bulkDelete(Math.min(parseInt(args[0]) + 1, 100));
const messages = await restClient.fetchMessages(message.channel_id, { limit: Math.min(parseInt(args[0]) + 1, 100) });
restClient.bulkDeleteMessages(message.channel_id, messages.map((x: APIMessage) => x.id));
} else {
restClient.createMessage(message.channel_id, `You have to tell me how many messages I should clean. \`${config.prefix}clean 10\` for example.`);
}
}
}

View file

@ -1,182 +0,0 @@
const { MessageEmbed, Message } = require('discord.js');
const CommandCategory = require('../model/command-category');
const CommandPermission = require('../model/command-permission');
const RGB_REGEX = /(\d{1,3})\D*,\D*(\d{1,3})\D*,\D*(\d{1,3})\D*/u;
class EmbedDialog
{
/**
* @param {Message} message
*/
constructor(message) {
this.embed = new MessageEmbed();
this.message = message;
this.channel = message.channel;
this.destinationChannel = null;
this.prompt = (question, hideSkip = false) => {
return this.channel.send(`${question}\n*${!hideSkip ? '`skip` to skip, ' : ''}\`cancel\` to cancel*`);
};
this.isMessageSkip = message => message.content.toLowerCase() === 'skip';
this.messageFilter = testedMessage => {
const byAuthor = testedMessage.author.id === message.author.id;
const hasContent = testedMessage.cleanContent.trim().length > 0;
return byAuthor && hasContent;
}
this.channelMessageFilter = testedMessage => {
return this.messageFilter(testedMessage)
&& (this.isMessageSkip(testedMessage) || testedMessage.mentions.channels.size > 0);
}
this.returnFirstOfCollection = collection => collection && collection.size ? collection.first() : null;
this.awaitOptions = { max: 1, time: 5 * MINUTE, errors: ['time'] };
/**
* @param {function} filter
* @returns {Promise<Message>}
*/
this.awaitMessage = (filter = this.messageFilter) => {
return this.channel.awaitMessages(
filter,
this.awaitOptions
).then(async collection => {
let message = this.returnFirstOfCollection(collection);
if (message && message.content.toLowerCase() === 'cancel') {
message = null;
await this.channel.send('Cancelling embed creation.');
}
return message;
}).catch(async () => {
await this.channel.send('Time out, cancelling embed creation.');
});
};
}
async execute() {
let confirmation = true;
await this.prompt('#️⃣ In which **channel** would you like this embed to be posted?');
const channelMessage = await this.awaitMessage(this.channelMessageFilter);
if (!channelMessage) {
return null;
}
if (this.isMessageSkip(channelMessage)) {
this.destinationChannel = this.channel;
confirmation = false;
} else {
this.destinationChannel = channelMessage.mentions.channels.first();
}
await this.prompt('📰 What do you want the **title** of this embed to be (you can also ping someone so it appears as they are saying what is going to be the description)?');
const titleMessage = await this.awaitMessage(this.messageFilter);
if (!titleMessage) {
return null;
}
if (!this.isMessageSkip(titleMessage)) {
if (titleMessage.mentions.members.size > 0) {
const member = titleMessage.mentions.members.first();
this.embed.setAuthor(member.displayName, member.user.displayAvatarURL({dynamic: true}));
} else {
this.embed.setTitle(titleMessage.content);
}
}
await this.prompt('🎨 What do you want the **color** of this embed to be?');
const colourMessage = await this.awaitMessage(this.messageFilter);
if (!colourMessage) {
return null;
}
if (this.isMessageSkip(colourMessage)) {
this.embed.setColor(APP_MAIN_COLOUR);
} else {
if (colourMessage.content.startsWith('#')) {
this.embed.setColor(parseInt(colourMessage.content.substr(1), 16));
} else if (colourMessage.content.startsWith('0x')) {
this.embed.setColor(parseInt(colourMessage.content.substr(2), 16));
} else if (RGB_REGEX.test(colourMessage.content)) {
const [, red, green, blue] = colourMessage.content.match(RGB_REGEX);
this.embed.setColor([parseInt(red), parseInt(green), parseInt(blue)]);
} else {
this.embed.setColor(colourMessage.content.toUpperCase().replace(/[^A-Z]+/gu, '_'));
}
}
await this.prompt('💬 What do you want the **description** (contents) of this embed to be?', true);
const descriptionMessage = await this.awaitMessage(this.messageFilter);
if (!descriptionMessage) {
return null;
}
this.embed.setDescription(descriptionMessage.content);
await this.destinationChannel.send(this.embed);
if (confirmation) {
return this.channel.send(`✅ The embed has been posted in ${this.destinationChannel}.`);
}
}
}
class Embed
{
static instance = null;
constructor() {
if (Embed.instance !== null) {
return Embed.instance;
}
this.aliases = [];
this.category = CommandCategory.MODERATION;
this.isAllowedForContext = CommandPermission.isMemberModOrHelper;
this.description = 'Allows to post an embed';
}
/**
* @param {Message} message
* @param {Array} args
*/
async process(message, args) {
if (args.length > 0) {
let destinationChannel = message.channel;
let deleteMessage = true;
if (/<#\d+>/u.test(args[0])) {
destinationChannel = message.mentions.channels.first();
args.shift();
deleteMessage = false;
}
const embed = new MessageEmbed();
embed.setColor(APP_MAIN_COLOUR);
embed.setDescription(args.join(' '));
await destinationChannel.send(embed);
if (deleteMessage) {
await message.delete();
} else {
await message.react('✅');
}
} else {
const dialog = new EmbedDialog(message);
return dialog.execute();
}
}
}
module.exports = new Embed();

View file

@ -1,40 +0,0 @@
const Discord = require('discord.js');
const Logger = require('@lilywonhalf/pretty-logger');
const Config = require('../config.json');
const CommandCategory = require('../model/command-category');
const CommandPermission = require('../model/command-permission');
// Including every model here so that it's ready to be used by the command
const Guild = require('../model/guild');
const JAVASCRIPT_LOGO_URL = 'https://i.discord.fr/IEV8.png';
module.exports = {
aliases: [],
category: CommandCategory.BOT_MANAGEMENT,
isAllowedForContext: CommandPermission.isMommy,
process: async (message) => {
const code = message.content
.substr(Config.prefix.length + 'eval'.length)
.trim()
.replace(/(`{3})js\n(.+)\n\1/iu, '$2')
.trim();
await message.react('✔');
Logger.notice('Eval: ' + code);
let output = null;
try {
output = eval(`${code}`); // Spoopy! 🎃 🦇 👻 ☠ 🕷
} catch (exception) {
output = `**${exception.name}: ${exception.message}**\n${exception.stack}`;
}
const embed = new Discord.MessageEmbed()
.setAuthor('Eval', JAVASCRIPT_LOGO_URL)
.setColor(0x00FF00)
.setDescription(output);
message.channel.send(embed).catch(error => Logger.warning(error.toString()));
}
};

40
command/eval.ts Normal file
View file

@ -0,0 +1,40 @@
import CommandCategory from "../model/command-category";
import * as CommandPermission from '../model/command-permission';
import config from "../config";
import { APIMessage } from "discord-api-types/v10";
import { restClient } from "../bot";
const JAVASCRIPT_LOGO_URL = 'https://i.discord.fr/IEV8.png';
export default {
aliases: ['eval'],
category: CommandCategory.BOT_MANAGEMENT,
isAllowedForContext: CommandPermission.isMommy,
process: async (message: APIMessage) => {
const code = message.content
.substr(config.prefix.length + 'eval'.length)
.trim()
.replace(/(`{3})js\n(.+)\n\1/iu, '$2')
.trim();
console.log('Eval: ' + code);
let output: string | null = null;
try {
output = eval(`${code}`); // Spoopy! 🎃 🦇 👻 ☠ 🕷
} catch (exception) {
// @ts-expect-error
output = `**${exception.name}: ${exception.message}**\n${exception.stack}`;
}
restClient.createMessage(message.channel_id, { embed: {
author: {
name: 'Eval',
iconUrl: JAVASCRIPT_LOGO_URL,
},
color: 0x00FF00,
description: output!,
}}).catch(error => console.warn(error.toString()));
}
};

View file

@ -1,132 +0,0 @@
const CommandPermission = require('../model/command-permission');
const CommandCategory = require('../model/command-category');
const commands = require('../model/command').commandList;
const messages = require('../model/messages');
const cachelessRequire = (path) => {
if (!typeof path === 'string') {
delete require.cache[require.resolve(path)];
}
return typeof path === 'string' ? require(path) : null;
};
const cleanString = (str) => str === null || str === undefined ? null : str
.replace('_', ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
const mainPage = (id) => {
const buttons = Object.keys(CommandCategory).map(c => ({
type: 2,
style: 1,
label: cleanString(c),
custom_id: `help-${id}-${c}`,
}))
return {
embeds: [{
title: 'Help command',
color: 0xA95B44,
description:
'Spot stands for **S**imply **P**lural b**ot**. This bot is mainly used to generate changelogs and help ' +
'with the definition of features, so recurring questions can be answered more quickly.\n' +
'\n' +
'Commands are classified by categories because displaying every command in this small box would be ' +
'confusing and would break Discord\'s character limit. Click on a buttom below to show the commands ' +
'available in the corresponding category, or type `.help <category name>`.\n'
}],
components: [{
'type': 1,
'components': buttons,
}]
}
}
const categoryPage = (cat, id, direct = false) => {
const c = Object.values(CommandCategory).map(x => cleanString(x).toLowerCase());
if (!c.includes(cat.toLowerCase()))
return;
let data = {
embeds: [{
title: `Help command | ${cleanString(cat)}`,
color: 0xA95B44,
}],
components: [{
'type': 1,
'components': [{
type: 2,
style: 2,
label: 'Back',
custom_id: `help-${id}-home`,
}],
}]
};
if (direct) delete data.components;
if (cat.toLowerCase() === cleanString(CommandCategory.RESOURCE).toLowerCase())
data.embeds[0].fields = messages.getList();
else
data.embeds[0].fields = Array.from(commands.keys()).map(x => {
const cmd = cachelessRequire(commands.get(x));
if (cleanString(cmd.category)?.toLowerCase() !== cat.toLowerCase()) return;
return { name: x, value: cmd.description ?? 'No description.'};
}).filter(x => x);
return data;
}
module.exports = {
aliases: ["commands"],
category: CommandCategory.INFO,
isAllowedForContext: () => true,
description: 'Provides the list of commands.',
process: async (message, args) => {
if (!args[0])
await bot.api.channels(message.channel.id).messages.post({ data: mainPage(message.author.id) });
else
await bot.api.channels(message.channel.id).messages.post({
data:
categoryPage(args.join(' '), message.author.id, true)
?? {
content: 'Category not found.',
message_reference: { message_id: message.id, guild_id: message.guild?.id },
allowed_mentions: { parse: [] },
}
})
}
}
module.exports.interactionHandler = async (event) => {
const user = event.member ? event.member.user.id : event.user.id;
const customId = event.data.custom_id;
let ret;
if (!customId.startsWith(`help-${user}`)) {
ret = {
type: 4,
data: {
flags: 64,
content: 'This help command was sent by someone else. Please run `.help` again.',
},
}
} else if (customId.endsWith('home')) {
ret = { type: 7, data: mainPage(user) };
} else {
let page = categoryPage(cleanString(customId.split(`help-${user}-`).join('')), user);
if (page) {
ret = { type: 7, data: page };
} else {
ret = { type: 4, data: { flags: 64, content: 'Category not found.' }}
}
}
return await bot.api.interactions(event.id, event.token).callback.post({ data: ret })
}

125
command/help.ts Normal file
View file

@ -0,0 +1,125 @@
import { restClient } from "../bot";
import CommandCategory from "../model/command-category";
import * as CommandPermission from '../model/command-permission';
import * as types from 'discord-api-types/v10';
import { Command, commands } from "../messageHandler";
import * as messages from "../model/messages";
import { GatewayMessageCreateDispatchData } from "discord-api-types/v10";
const cleanString = (str: string | null | undefined) => (str === null || str === undefined) ? undefined : str
.replace('_', ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
const mainPage = (id: string) => {
const buttons = Object.values(CommandCategory).filter(x => typeof x === 'string').map(c => ({
type: 2,
style: 1,
label: cleanString(c),
customId: `help-${id}-${c}`,
}))
return {
embeds: [{
title: 'Help command',
color: 0xA95B44,
description:
'Spot stands for **S**imply **P**lural b**ot**. This bot is mainly used to generate changelogs and help ' +
'with the definition of features, so recurring questions can be answered more quickly.\n' +
'\n' +
'Commands are classified by categories because displaying every command in this small box would be ' +
'confusing and would break Discord\'s character limit. Click on a buttom below to show the commands ' +
'available in the corresponding category, or type `.help <category name>`.\n'
}],
components: [{
'type': 1,
'components': buttons,
}]
}
}
const categoryPage = (cat: string, id: string, direct = false) => {
const c = Object.values(CommandCategory).filter(x => typeof x === 'string').map(x => cleanString(x)?.toLowerCase());
if (!c.includes(cat.toLowerCase()))
return;
let data = {
embeds: [{
title: `Help command | ${cleanString(cat)}`,
color: 0xA95B44,
}],
components: [{
'type': 1,
'components': [{
type: 2,
style: 2,
label: 'Back',
customId: `help-${id}-home`,
}],
}],
};
// @ts-expect-error
if (direct) delete data.components;
if (cat == CommandCategory.RESOURCE)
// @ts-expect-error
data.embeds[0].fields = messages.getList();
else
// @ts-expect-error
data.embeds[0].fields = commands.map((cmd: Command) => {
if (cmd.category !== cat) return;
return { name: cmd.aliases[0], value: cmd.description ?? 'No description.'};
}).filter(x => x);
return data;
}
export default {
aliases: ['help', "commands", 'h'],
category: CommandCategory.INFO,
isAllowedForContext: (_: GatewayMessageCreateDispatchData) => true,
description: 'Provides the list of commands.',
process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
if (!args[0])
await restClient.createMessage(message.channel_id, mainPage(message.author.id) );
else
await restClient.createMessage(message.channel_id, categoryPage(args.join(' '), message.author.id, true)
?? {
content: 'Category not found.',
messageReference: { messageId: message.id, guildId: message.guild_id },
allowedMentions: { parse: [] },
})
},
interactionHandler: async (event: types.APIMessageComponentInteraction) => {
const user = event.member ? event.member.user.id : event.user!.id;
const customId = event.data.custom_id;
let ret;
if (!customId.startsWith(`help-${user}`)) {
ret = {
type: 4,
data: {
flags: 64,
content: 'This help command was sent by someone else. Please run `.help` again.',
},
}
} else if (customId.endsWith('home')) {
ret = { type: 7, data: mainPage(user) };
} else {
let page = categoryPage(cleanString(customId.split(`help-${user}-`).join(''))!, user);
if (page) {
ret = { type: 7, data: page };
} else {
ret = { type: 4, data: { flags: 64, content: 'Category not found.' }}
}
}
return await restClient.createInteractionResponse(event.id, event.token, ret);
}
};

View file

@ -1,36 +1,38 @@
const CommandCategory = require('../model/command-category');
const CommandPermission = require('../model/command-permission');
import CommandCategory from "../model/command-category";
import * as CommandPermission from '../model/command-permission';
const axios = require('axios');
import axios from "axios";
import { APIMessage, GatewayMessageCreateDispatchData } from "discord-api-types/v10";
import { restClient } from "../bot";
module.exports = {
aliases: [],
export default {
aliases: ['ticket'],
category: CommandCategory.MODERATION,
isAllowedForContext: CommandPermission.isMemberModOrHelper,
process: async (message) => {
if (message.reference == null)
return message.reply("missing reply");
process: async (message: GatewayMessageCreateDispatchData, args: string[]) => {
if (message.message_reference == null)
return restClient.createMessage(message.channel_id, `<@${message.author.id}>, missing reply`);
let reply = await message.channel.messages.fetch(message.reference.messageID);
let reply: APIMessage = await restClient.fetchMessage(message.channel_id, message.message_reference.message_id!);
let user = null;
if (reply.webhookID != null) {
if (reply.webhook_id != null) {
let pkmsg = await axios(`https://api.pluralkit.me/v2/messages/${reply.id}`);
user = pkmsg.data.sender;
} else {
user = reply.author.id;
}
message.delete();
message.client.api.channels(message.channel.id).messages.post({ data: {
restClient.deleteMessage(message.channel_id, message.id).catch(() => {});
restClient.createMessage(message.channel_id, {
content: `<@${user}>: Please send us a support ticket for your specific issue using this link: <https://apparyllis.atlassian.net/servicedesk/customer/portal/3>\n`
+ `You can also click the button below this message.\n\n`
+ `Select the support type that best fits your issue. Make sure to fill out any information that is requested.\n\n`
+ `Once you fill out the ticket, you will receive an email with a confirmation.\nWhen the developer answers the ticket, you will get another email, `
+ `so please watch your email (and spam folder) for a reply.`,
message_reference: { message_id: reply.id },
messageReference: { messageId: reply.id },
components: [{ type: 1, components: [{ type: 2, style: 5, label: "Submit ticket", url: "https://apparyllis.atlassian.net/servicedesk/customer/portals" }]}]
}});
});
}
};