mirror of
https://github.com/Team-Neptune/Korral-Interactions.git
synced 2024-11-23 04:39:39 +00:00
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:
parent
7c3b0e78a5
commit
6baef73906
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -7,7 +7,7 @@
|
||||
{
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"name": "Start Webserver",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
|
32
src/buttons/close_ticket_.ts
Normal file
32
src/buttons/close_ticket_.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
56
src/buttons/open_private_ticket.ts
Normal file
56
src/buttons/open_private_ticket.ts
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
56
src/buttons/open_ticket.ts
Normal file
56
src/buttons/open_ticket.ts
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
39
src/commands/setup_ticket_button.ts
Normal file
39
src/commands/setup_ticket_button.ts
Normal 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
62
src/commands/ticket.ts
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
85
src/index.ts
85
src/index.ts
@ -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
23
typings/index.d.ts
vendored
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user