Add tickets feature (#11)

* Update typings to reflect new functions, props, and interfaces

* Update index.ts

* Create ticket.ts

* Added `lockThread` function

A function to easily lock a thread.

* Update launch.json

* Add option for authorized users to close ticket

* Add command to setup message to open tickets

* Disallow creation of multiple tickets

* Updated unauthorized ticket closure message

* Update Ticket Creation message

* Updated to have function dedicated to closing tickets

* Update message if ticket already exists

* Fix incorrect channel showing for setup_ticket_button

* Update setup_ticket_button message to be more specific

* Add support for "private" tickets

* Update starter message

* Fix bug with closing tickets

* Add ability to set ticket as "private"

* Change emoji representing public/private tickets

* Better specify whether a ticket is public/private
This commit is contained in:
Daniel 2021-10-06 15:58:32 -07:00 committed by GitHub
parent 7c3b0e78a5
commit 6baef73906
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 351 additions and 4 deletions

2
.vscode/launch.json vendored
View File

@ -7,7 +7,7 @@
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"name": "Start Webserver",
"skipFiles": [
"<node_internals>/**"
],

View File

@ -0,0 +1,32 @@
import ButtonCommand from "../classes/ButtonCommand";
export default new ButtonCommand({
checkType:"STARTS_WITH",
execute(interaction){
let ticketUserId = interaction.data.custom_id.split("close_ticket_")[1];
let currentUserId = interaction.member.user.id;
let threadChannelId = interaction.message.channel_id;
if(currentUserId != ticketUserId && !interaction.member.roles.includes(interaction.internalBot.config.supportRoleId))
return interaction.reply({
content:`You can't close a ticket that isn't yours.`,
ephemeral:true
})
interaction.reply({
content:`<#${threadChannelId}> will be locked soon.`,
ephemeral:true
})
.then(() => {
interaction.sendMessage(threadChannelId, {
embeds:[
{
"description":`🔒 Ticket has been closed by <@${currentUserId}>`,
"color":16711680
}
]
})
.then(() => {
return interaction.closeSupportThread(threadChannelId, ticketUserId)
})
})
}
})

View File

@ -0,0 +1,56 @@
import ButtonCommand from "../classes/ButtonCommand";
export default new ButtonCommand({
checkType:"EQUALS",
execute(interaction){
let topic = {
value:`${interaction.member.user.username}#${interaction.member.user.discriminator}`
}
if(topic.value.length > 90 || topic.value.length < 1)
return interaction.reply({
content:`Topic must be 1-90 characters`,
ephemeral:true
})
interaction.ack({
"ephemeral":true
}).then(() => {
interaction.createSupportThread(topic.value, interaction.member.user.id, true)
.then(channel => {
// Error
if(typeof channel == 'string')
return interaction.reply({
content:channel,
ephemeral:true
}).catch(console.log)
interaction.joinThread(channel.id).then(res => {
interaction.sendMessage(channel.id, {
"content":
`Hey <@${interaction.member.user.id}>,\n<@&${interaction.internalBot.config.supportRoleId}> will be here to support you shortly. In the meantime, to make it easier for us and others help you with your issue, please tell us a few things about your setup, like:\n\n- Firmware and CFW / Atmosphere / DeepSea version\n- Do you use hekate or fusee-primary?\n- If you have an error screen with ID or code, what does it say? A screenshot/picture could be helpful.\n- What, if anything, have you tried to fix the issue?\n\n*(Disclaimer: You may not receive an answer instantly. Many of us have lives outside of Discord and will respond whenever we're able to, whenever that is.)*\n\n:lock: *This is a private ticket, so only staff may reply.*`,
"components":[
{
"type":1,
"components":[
{
"type":2,
"style":2,
"custom_id":`close_ticket_${interaction.member.user.id}`,
"label":"Close Ticket",
"emoji":{
"name":"🔒"
}
}
]
}
]
}).then(r => {
interaction.reply({
content:`Ticket is ready in <#${channel.id}>`,
ephemeral:true
})
})
})
})
})
}
})

View File

@ -0,0 +1,56 @@
import ButtonCommand from "../classes/ButtonCommand";
export default new ButtonCommand({
checkType:"EQUALS",
execute(interaction){
let topic = {
value:`${interaction.member.user.username}#${interaction.member.user.discriminator}`
}
if(topic.value.length > 90 || topic.value.length < 1)
return interaction.reply({
content:`Topic must be 1-90 characters`,
ephemeral:true
})
interaction.ack({
"ephemeral":true
}).then(() => {
interaction.createSupportThread(topic.value, interaction.member.user.id, false)
.then(channel => {
// Error
if(typeof channel == 'string')
return interaction.reply({
content:channel,
ephemeral:true
}).catch(console.log)
interaction.joinThread(channel.id).then(res => {
interaction.sendMessage(channel.id, {
"content":
`Hey <@${interaction.member.user.id}>,\n<@&${interaction.internalBot.config.supportRoleId}> will be here to support you shortly. In the meantime, to make it easier for us and others help you with your issue, please tell us a few things about your setup, like:\n\n- Firmware and CFW / Atmosphere / DeepSea version\n- Do you use hekate or fusee-primary?\n- If you have an error screen with ID or code, what does it say? A screenshot/picture could be helpful.\n- What, if anything, have you tried to fix the issue?\n\n*(Disclaimer: You may not receive an answer instantly. Many of us have lives outside of Discord and will respond whenever we're able to, whenever that is.)*\n\n:unlock: *This is a public ticket, everyone may view and reply to it..*`,
"components":[
{
"type":1,
"components":[
{
"type":2,
"style":2,
"custom_id":`close_ticket_${interaction.member.user.id}`,
"label":"Close Ticket",
"emoji":{
"name":"🔒"
}
}
]
}
]
}).then(r => {
interaction.reply({
content:`Ticket is ready in <#${channel.id}>`,
ephemeral:true
})
})
})
})
})
}
})

View File

@ -0,0 +1,39 @@
import Command from "../classes/Command";
export default new Command({
execute(interaction){
interaction.reply({
"content":`Ready to open tickets from <#${interaction.internalBot.config.supportChannelId}>`,
"ephemeral":true
}).then(() => {
interaction.sendMessage(interaction.internalBot.config.supportChannelId, {
content:"**Check for your issue**\nBefore opening a ticket, check the thread menu <:threadmenu:894295426176520283> and search/scroll through the __**Archived**__ tab/button at the top of the thread menu.\n\n**If you can't find your issue**\n\n*Open Public Support Ticket* : Allows everyone to view your ticket and provide support.\n\n*Open Private Support Ticket* : Allows everyone to view your ticket, but only staff members can provide support.\n\n*`/ticket` Slash Command* : Run in any other channel to open a support ticket supplied with a specific topic.\n\n**Want to help others?**\nOpen the thread menu <:threadmenu:894295426176520283> and search/scroll through the __**Active**__ threads for people needing help. Please only join a thread if you intend to provide support.",
components:[
{
type:1,
components:[
{
"custom_id":`open_ticket`,
"label":"Open Public Support Ticket",
"type":2,
"style":2,
"emoji":{
"name":"🔓"
}
},
{
"custom_id":`open_private_ticket`,
"label":"Open Private Support Ticket",
"type":2,
"style":2,
"emoji":{
"name":"🔒"
}
}
]
}
]
}).then(r => r.json()).then(console.log)
})
}
})

62
src/commands/ticket.ts Normal file
View File

@ -0,0 +1,62 @@
import Command from "../classes/Command";
export default new Command({
execute(interaction){
// 1-90 char only
let topic = interaction.data.options.find(o => o.name == "topic");
let supportRoleOnly = interaction.data.options.find(o => o.name == "private") && interaction.data.options.find(o => o.name == "private").value || false;
let threadStarter = interaction.member.user.id;
if(topic.value.length > 90 || topic.value.length < 1)
return interaction.reply({
content:`Topic must be 1-90 characters`,
ephemeral:true
})
interaction.ack({
"ephemeral":true
}).then(() => {
interaction.createSupportThread(topic.value, threadStarter, supportRoleOnly)
.then(channel => {
// Error
if(typeof channel == 'string')
return interaction.reply({
content:channel,
ephemeral:true
}).catch(console.log)
interaction.joinThread(channel.id).then(res => {
interaction.sendMessage(channel.id, {
"content":
`Hey <@${threadStarter}>,\n<@&${interaction.internalBot.config.supportRoleId}> will be here to support you shortly. In the meantime, to make it easier for us and others help you with your issue, please tell us a few things about your setup, like:\n\n- Firmware and CFW / Atmosphere / DeepSea version\n- Do you use hekate or fusee-primary?\n- If you have an error screen with ID or code, what does it say? A screenshot/picture could be helpful.\n- What, if anything, have you tried to fix the issue?\n\n*(Disclaimer: You may not receive an answer instantly. Many of us have lives outside of Discord and will respond whenever we're able to, whenever that is.)*\n${supportRoleOnly?"\n:lock: *This is a private ticket, so only staff may reply.*":"\n:unlock: *This is a public ticket, everyone may view and reply to it..*"}`,
"components":[
{
"type":1,
"components":[
{
"type":2,
"style":2,
"custom_id":`close_ticket_${threadStarter}`,
"label":"Close Ticket",
"emoji":{
"name":"🔒"
}
}
]
}
]
}).then(r => {
interaction.reply({
content:`Ticket is ready in <#${channel.id}>`,
ephemeral:true
})
})
})
})
.catch(err => {
if(typeof err == "string")
interaction.reply({
content:err,
ephemeral:true
})
})
})
}
})

View File

@ -12,6 +12,9 @@ import {
BuilderApiModule,
SDLayoutOS,
InteractionAckOptions,
User,
ThreadCreateOptions,
Message,
} from "../typings"
import express from "express"
import {
@ -68,6 +71,8 @@ checkForLatestBuildApi()
app.use("/interactions", verifyKeyMiddleware(public_key))
let hasActiveTickets = {}
//Set props/methods
app.use("/interactions", (req, res, next) => {
const interaction: Interaction = req.body
@ -82,6 +87,7 @@ app.use("/interactions", (req, res, next) => {
}
req.body.ack = (options?:InteractionAckOptions) => {
return new Promise((res, rej) => {
interaction.acked = true;
fetch(`${discord_api}/interactions/${interaction.id}/${interaction.token}/callback`, {
"method":"POST",
"headers":{
@ -96,22 +102,97 @@ app.use("/interactions", (req, res, next) => {
.catch(rej)
})
}
req.body.sendMessage = (channelId:string, msg:Message) => {
return new Promise((res, rej) => {
fetch(`${discord_api}/channels/${channelId}/messages`, {
"method":"POST",
"headers":{
"authorization":`Bot ${config.bot_token}`,
"Content-Type":"application/json"
},
"body":JSON.stringify(msg)
})
.then(res)
.catch(rej)
})
}
req.body.reply = (options:InteractionResponse) => {
if(options.ephemeral)
options.flags = 64
return new Promise((res, rej) => {
fetch(`${discord_api}/interactions/${interaction.id}/${interaction.token}/callback`, {
fetch(`${discord_api}${interaction.acked?`/webhooks/${interaction.application_id}/${interaction.token}`:`/interactions/${interaction.id}/${interaction.token}/callback`}`, {
"method":"POST",
"headers":{
"Content-Type":"application/json"
},
"body":JSON.stringify({
"body":JSON.stringify(interaction.acked?options:{
type:4,
data:options
})
})
.then(res)
.then(rej)
interaction.acked = true;
})
}
req.body.lockThread = (channelId:string) => {
return new Promise((resolve, reject) => {
fetch(`${discord_api}/channels/${channelId}`, {
"method":"PATCH",
headers:{
"authorization":`Bot ${config.bot_token}`,
"Content-Type":"application/json"
},
body:JSON.stringify({
archived:true,
locked:true
})
})
.then(resolve)
.catch(reject)
})
}
req.body.createSupportThread = (shortDesc:string, userId:string, privateTicket:boolean) => {
let options:ThreadCreateOptions = {
name:`${privateTicket?"🔒":"🔓"} - ${shortDesc}`,
auto_archive_duration:1440,
type:11
}
return new Promise((resolve, reject) => {
if(hasActiveTickets[userId])
return resolve(`You already have a ticket opened. Please close your current ticket to open a new one.`)
hasActiveTickets[userId] = true;
fetch(`${discord_api}/channels/${config.supportChannelId}/threads`, {
"method":"POST",
headers:{
"authorization":`Bot ${config.bot_token}`,
"Content-Type":"application/json"
},
body:JSON.stringify(options)
})
.then(r => r.json())
.then(resolve)
.catch(reject)
})
}
req.body.closeSupportThread = (channelId:string, userId:string) => {
hasActiveTickets[userId] = false;
return new Promise((resolve, reject) => {
interaction.lockThread(channelId)
.then(resolve)
.catch(reject)
})
}
req.body.joinThread = (channelId:string) => {
return new Promise((resolve, reject) => {
fetch(`${discord_api}/channels/${channelId}/thread-members/@me`, {
"method":"PUT",
headers:{
"authorization":`Bot ${config.bot_token}`
}
})
.then(resolve)
.catch(reject)
})
}
if(interaction.type == InteractionType.MESSAGE_COMPONENT){

23
typings/index.d.ts vendored
View File

@ -1,5 +1,18 @@
import Builder from "../src/builder";
interface Message {
content?: string,
components?: MessageComponent[],
allowed_mentions?:AllowedMentions,
embeds?:any[]
}
interface ThreadCreateOptions {
name:string,
auto_archive_duration:60 | 1440,
type:11
}
interface ApplicationCommandInteractionDataOption {
name:string,
type:number,
@ -69,9 +82,15 @@ export interface Interaction {
token:string,
version:number,
message?:any,
acked:boolean,
update?(msg:InteractionResponse):Promise<Response>,
reply(msg:InteractionResponse):Promise<Response>,
ack(msg:InteractionAckOptions):Promise<Response>,
createSupportThread(shortDesc:string, userId:string, privateTicket:boolean):Promise<any>,
closeSupportThread(channelId:string, userId:string):Promise<any>,
sendMessage(channelId:string, msg:Message),
joinThread(channelId:string):Promise<Response>,
lockThread(channelId:string):Promise<Response>,
packageBuilder:{
builder:Builder,
store:Builder,
@ -120,7 +139,9 @@ export interface Config {
public_key:string,
port?:number,
bot_token:string,
bitly_token?:string
bitly_token?:string,
supportChannelId?:string,
supportRoleId?:string
}
export interface GitHubRelease {