From d0404bd46d24a8625c280d8edaa389270e8d93b0 Mon Sep 17 00:00:00 2001
From: Levatax <levatax@randomchars.net>
Date: Fri, 9 Jul 2021 13:19:46 +0300
Subject: [PATCH] fix guild ID handling, optional user information to ticket
 setup, persisting of ticket state on disk

---
 .gitignore |  1 +
 cleanup.go |  7 +++---
 config.go  |  2 ++
 handler.go | 13 ++++++-----
 main.go    | 19 ++++++++++++++++
 ticket.go  | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 6 files changed, 96 insertions(+), 10 deletions(-)

diff --git a/.gitignore b/.gitignore
index 6cb095e..eefd3c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,6 @@
 *.test
 *.out
 /.idea/
+/store/
 ticket-bot
 ticket.conf
\ No newline at end of file
diff --git a/cleanup.go b/cleanup.go
index 21e2de3..b200b8a 100644
--- a/cleanup.go
+++ b/cleanup.go
@@ -2,12 +2,13 @@ package main
 
 import (
 	log "git.randomchars.net/FreeNitori/Log"
-	"os"
 )
 
 func cleanup() {
+	dumpPath := config.Store + "/state"
 	if err := session.Close(); err != nil {
-		log.Fatalf("Error while closing session, %s", err)
-		os.Exit(1)
+		log.Warnf("Error while closing session, %s", err)
 	}
+	log.Infof("Dumping ticket state to %s.", dumpPath)
+	dumpTicketState(dumpPath)
 }
diff --git a/config.go b/config.go
index 6c66f4c..dc1b2ec 100644
--- a/config.go
+++ b/config.go
@@ -14,6 +14,7 @@ var defaultConfig = configPayload{
 	MessageID: []string{},
 	GuildID:   "GUILD_ID",
 	Prefix:    "!",
+	Store:     "./store",
 }
 
 type configPayload struct {
@@ -21,6 +22,7 @@ type configPayload struct {
 	MessageID []string
 	GuildID   string
 	Prefix    string
+	Store     string
 }
 
 func init() {
diff --git a/handler.go b/handler.go
index 91df2de..c0ce299 100644
--- a/handler.go
+++ b/handler.go
@@ -19,7 +19,7 @@ func handleReaction(context *multiplexer.Context) {
 		return
 	}
 
-	if reactionAdd.GuildID != config.GuildID {
+	if reactionAdd.GuildID == "" {
 		return
 	}
 	if reactionAdd.UserID == session.State.User.ID {
@@ -31,13 +31,13 @@ func handleReaction(context *multiplexer.Context) {
 
 	switch reactionAdd.Emoji.Name {
 	case "1️⃣":
-		setupTicket(reactionAdd.GuildID, reactionAdd.UserID, 0)
+		setupTicket(config.GuildID, reactionAdd.UserID, 0, context.User)
 	case "2️⃣":
-		setupTicket(reactionAdd.GuildID, reactionAdd.UserID, 1)
+		setupTicket(config.GuildID, reactionAdd.UserID, 1, context.User)
 	case "3️⃣":
-		setupTicket(reactionAdd.GuildID, reactionAdd.UserID, 2)
+		setupTicket(config.GuildID, reactionAdd.UserID, 2, context.User)
 	case "4️⃣":
-		setupTicket(reactionAdd.GuildID, reactionAdd.UserID, 3)
+		setupTicket(config.GuildID, reactionAdd.UserID, 3, context.User)
 	default:
 		return
 	}
@@ -60,6 +60,9 @@ func handleMessages(context *multiplexer.Context) {
 	case true:
 		sendMessage(instance.ChannelID, context.Message.Content)
 	case false:
+		if context.Message.Content == context.Prefix() + "close" {
+			return
+		}
 		sendMessage(instance.UserChannelID, context.Message.Content)
 	}
 }
diff --git a/main.go b/main.go
index 26f5599..65b5469 100644
--- a/main.go
+++ b/main.go
@@ -20,6 +20,19 @@ func main() {
 	flag.Parse()
 	parse()
 
+	// Ensure store directory
+	if _, err := os.ReadDir(config.Store); err != nil {
+		if os.IsNotExist(err) {
+			log.Warnf("Creating store directory on %s.", config.Store)
+			if err = os.MkdirAll(config.Store, 0700); err != nil {
+				log.Fatalf("Error creating store directory on %s, %s", config.Store, err)
+				os.Exit(1)
+			}
+		} else {
+			log.Fatalf("Store directory inaccessible, %s.", err)
+		}
+	}
+
 	// Set discordgo log handler
 	discordgo.Logger = func(msgL, _ int, format string, a ...interface{}) {
 		var level logrus.Level
@@ -53,6 +66,12 @@ func main() {
 	session.ShouldReconnectOnError = true
 	session.Identify.Intents = discordgo.IntentsAllWithoutPrivileged
 
+	if state, ok := readTicketState(config.Store + "/state"); ok == true {
+		ticketInstancesUser = state.TicketInstancesUser
+		ticketInstancesChannel = state.TicketInstancesChannel
+		log.Infof("Read %v ticket states from %s.", len(ticketInstancesChannel), config.Store + "/state")
+	}
+
 	// Open session
 	if err := session.Open(); err != nil {
 		log.Fatalf("Error while opening session, %s", err)
diff --git a/ticket.go b/ticket.go
index 8e63004..2c23527 100644
--- a/ticket.go
+++ b/ticket.go
@@ -1,12 +1,16 @@
 package main
 
 import (
+	"encoding/json"
 	"fmt"
 	log "git.randomchars.net/FreeNitori/Log"
 	"github.com/bwmarrin/discordgo"
+	"os"
+	"sync"
 	"time"
 )
 
+var ticketLock = sync.RWMutex{}
 var ticketInstancesChannel = make(map[string]*ticket)
 var ticketInstancesUser = make(map[string]*ticket)
 
@@ -17,7 +21,15 @@ type ticket struct {
 	CreationDate  time.Time `json:"creation_date"`
 }
 
+type ticketState struct {
+	TicketInstancesChannel map[string]*ticket `json:"ticket_instances_channel"`
+	TicketInstancesUser    map[string]*ticket `json:"ticket_instances_user"`
+}
+
 func (t *ticket) delete() {
+	ticketLock.RLock()
+	defer ticketLock.RUnlock()
+
 	delete(ticketInstancesUser, t.UserID)
 	delete(ticketInstancesChannel, t.ChannelID)
 	if _, err := session.ChannelDelete(t.ChannelID); err != nil {
@@ -50,7 +62,9 @@ func createTicket(channelID, userID string) *ticket {
 	return instance
 }
 
-func setupTicket(guildID, userID string, subject int) bool {
+func setupTicket(guildID, userID string, subject int, user *discordgo.User) bool {
+	ticketLock.RLock()
+	defer ticketLock.RUnlock()
 	if channel, err := session.GuildChannelCreate(guildID, "ticket-"+userID, discordgo.ChannelTypeGuildText); err != nil {
 		log.Warnf("Error creating ticket channel for user %s, %s", userID, err)
 		return false
@@ -64,6 +78,52 @@ func setupTicket(guildID, userID string, subject int) bool {
 				return false
 			}
 		}
-		return sendMessage(channel.ID, fmt.Sprintf("Ticket created with subject %v.", subject))
+		if user != nil {
+			return sendMessage(channel.ID, fmt.Sprintf("Ticket created by user %s#%s (%s) with subject %v.",
+				user.Username, user.Discriminator, user.ID, subject))
+		} else {
+			return sendMessage(channel.ID, fmt.Sprintf("Ticket created with subject %v.", subject))
+		}
+	}
+}
+
+func makeActiveTicketsPayload() []byte {
+	if payload, err := json.Marshal(ticketState{
+		TicketInstancesChannel: ticketInstancesChannel,
+		TicketInstancesUser:    ticketInstancesUser,
+	}); err != nil {
+		log.Fatalf("Error saving ticket state, %s, all ticket changes since previous start will be lost.")
+		os.Exit(1)
+	} else {
+		return payload
+	}
+	return nil
+}
+
+func dumpTicketState(path string) {
+	// Permanently lock ticket state until program exit
+	ticketLock.Lock()
+
+	if err := os.WriteFile(path, makeActiveTicketsPayload(), 0600); err != nil {
+		log.Errorf("Error dumping ticket state, %s, all ticket changes since previous start will be lost.")
+	}
+}
+
+func readTicketState(path string) (ticketState, bool) {
+	var state ticketState
+	if payload, err := os.ReadFile(path); err != nil {
+		if os.IsNotExist(err) {
+			return state, false
+		}
+		log.Fatalf("Error reading state file, %s", err)
+		os.Exit(1)
+	} else {
+		if err = json.Unmarshal(payload, &state); err != nil {
+			log.Fatalf("Error parsing state file, %s", err)
+			os.Exit(1)
+		} else {
+			return state, true
+		}
 	}
+	return state, false
 }
-- 
GitLab