Ticket bot (Go)
Opens a private support channel for each member who runs /ticket, with a button to close it.
Go 1.23
♥ 0 likes
Environment variables you'll need
| Key | What it is |
|---|---|
DISCORD_TOKEN | Your bot's token from the Discord Developer Portal. Paste it on the Secrets tab; we store it encrypted. |
TICKET_CATEGORY_ID | Optional. Category the ticket channels are created in |
SUPPORT_ROLE_ID | Optional. Role that can see and close every ticket |
Files
README.md
# Ticket bot
A Go bot built on discordgo. Your bot token is set on the Secrets tab as
DISCORD_TOKEN and reaches the bot as an environment variable.
## Commands
- `/ticket [reason]` opens a private channel only you, the bot and the support role can see. One open ticket per member.
- The Close button (or `/close` inside the ticket) deletes the channel.
## Setup
1. No privileged gateway intents are needed. Leave Message Content, Server Members
and Presence off in the Discord Developer Portal.
2. Invite the bot with the `bot` and `applications.commands` scopes and these
permissions: View Channels, Manage Channels, Send Messages, Read Message History.
3. Set these values on the Secrets tab. The ones marked optional can stay empty:
- `TICKET_CATEGORY_ID`: Optional. Category the ticket channels are created in.
- `SUPPORT_ROLE_ID`: Optional. Role that can see and close every ticket.
4. Press Deploy. The console shows a line with "Logged in as" once the bot is online.
go.mod
module example.com/ticket-bot-go
go 1.23
require github.com/bwmarrin/discordgo v0.28.1
go.sum
main.go
package main
import (
"fmt"
"log"
"os"
"os/signal"
"regexp"
"slices"
"strings"
"syscall"
"github.com/bwmarrin/discordgo"
)
const closeID = "ticket:close"
var (
digits = regexp.MustCompile(`^\d+$`)
categoryID = envID("TICKET_CATEGORY_ID")
supportRoleID = envID("SUPPORT_ROLE_ID")
noDM = false
)
var commands = []*discordgo.ApplicationCommand{
{Name: "ticket", Description: "Open a private support ticket", DMPermission: &noDM,
Options: []*discordgo.ApplicationCommandOption{{
Type: discordgo.ApplicationCommandOptionString, Name: "reason",
Description: "What do you need help with?"}}},
{Name: "close", Description: "Close this ticket", DMPermission: &noDM},
}
func envID(name string) string {
v := strings.TrimSpace(os.Getenv(name))
if digits.MatchString(v) {
return v
}
return ""
}
func ticketName(userID string) string { return "ticket-" + userID }
func reply(s *discordgo.Session, i *discordgo.InteractionCreate, content string, ephemeral bool) error {
data := &discordgo.InteractionResponseData{Content: content}
if ephemeral {
data.Flags = discordgo.MessageFlagsEphemeral
}
return s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Data: data})
}
func openTicket(s *discordgo.Session, i *discordgo.InteractionCreate) error {
user := i.Member.User
channels, err := s.GuildChannels(i.GuildID)
if err != nil {
return err
}
parentID := ""
for _, c := range channels {
if c.Name == ticketName(user.ID) {
return reply(s, i, "You already have an open ticket: <#"+c.ID+">", true)
}
if c.ID == categoryID && c.Type == discordgo.ChannelTypeGuildCategory {
parentID = c.ID
}
}
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{Flags: discordgo.MessageFlagsEphemeral}})
if err != nil {
return err
}
var basic int64 = discordgo.PermissionViewChannel | discordgo.PermissionSendMessages |
discordgo.PermissionReadMessageHistory
overwrites := []*discordgo.PermissionOverwrite{
{ID: i.GuildID, Type: discordgo.PermissionOverwriteTypeRole, Deny: discordgo.PermissionViewChannel},
{ID: user.ID, Type: discordgo.PermissionOverwriteTypeMember, Allow: basic | discordgo.PermissionAttachFiles},
{ID: s.State.User.ID, Type: discordgo.PermissionOverwriteTypeMember, Allow: basic | discordgo.PermissionManageChannels},
}
if supportRoleID != "" {
if _, err := s.State.Role(i.GuildID, supportRoleID); err == nil {
overwrites = append(overwrites, &discordgo.PermissionOverwrite{
ID: supportRoleID, Type: discordgo.PermissionOverwriteTypeRole, Allow: basic})
}
}
channel, err := s.GuildChannelCreateComplex(i.GuildID, discordgo.GuildChannelCreateData{
Name: ticketName(user.ID), Type: discordgo.ChannelTypeGuildText, ParentID: parentID,
Topic: fmt.Sprintf("Ticket for %s (%s)", user.Username, user.ID),
PermissionOverwrites: overwrites,
})
if err != nil {
return err
}
content := fmt.Sprintf("<@%s> opened a ticket.", user.ID)
for _, opt := range i.ApplicationCommandData().Options {
if opt.Name == "reason" && opt.StringValue() != "" {
content += "\nReason: " + opt.StringValue()
}
}
content += "\nSupport will be with you soon. Press the button or use /close when done."
_, err = s.ChannelMessageSendComplex(channel.ID, &discordgo.MessageSend{
Content: content,
AllowedMentions: &discordgo.MessageAllowedMentions{Users: []string{user.ID}},
Components: []discordgo.MessageComponent{discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.Button{Label: "Close ticket", Style: discordgo.DangerButton, CustomID: closeID},
}}},
})
if err != nil {
return err
}
done := "Your ticket is ready: <#" + channel.ID + ">"
_, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{Content: &done})
return