From c86ad1c07eabfce72020efaad2283c66866d783f Mon Sep 17 00:00:00 2001
From: Levatax <levatax@randomchars.net>
Date: Sun, 8 Aug 2021 11:21:15 +0300
Subject: [PATCH] implement scoped rate limiting, wrap around instance actions,
 implement channel name changing command

---
 command.go  | 108 +++++++++++++++++++++++++++++++++++++++++++---------
 instance.go |  43 ++++++++++++++++++---
 2 files changed, 126 insertions(+), 25 deletions(-)

diff --git a/command.go b/command.go
index fccb674..8496a0d 100644
--- a/command.go
+++ b/command.go
@@ -5,6 +5,7 @@ import (
 	"git.randomchars.net/freenitori/multiplexer"
 	"github.com/bwmarrin/discordgo"
 	"strconv"
+	"strings"
 )
 
 func init() {
@@ -40,41 +41,110 @@ func init() {
 				return
 			}
 
-			// Lock the instance for write
-			instance.Lock()
-			defer instance.Unlock()
+			// Perform action
+			if !instance.actionPerform("ceiling",
+				func() bool {
+					// Check if payload is the same as current state
+					if instance.Channel.UserLimit == int(limit) {
+						context.SendMessage("User limit was not changed.")
+						return false
+					}
 
-			// Check if action is free
-			if !instance.actionFree() {
-				context.SendMessage("Not so fast! Please try again later.")
+					// Perform change
+					if channel, err := context.Session.ChannelEditComplex(instance.Channel.ID,
+						&discordgo.ChannelEdit{UserLimit: int(limit), Position: instance.Channel.Position}); !context.HandleError(err) {
+						return false
+					} else {
+						instance.Channel = channel
+					}
+					return true
+				}, context.SendMessage) {
 				return
 			}
 
-			// Check if payload is the same as current state
-			if instance.Channel.UserLimit == int(limit) {
-				context.SendMessage("User limit was not changed.")
+			log.Infof("Set user limit of volatile channel #%s (%s) to %v on behalf of %s#%s (%s).",
+				instance.Channel.Name,
+				instance.Channel.ID,
+				limit,
+				instance.Initiator.User.Username,
+				instance.Initiator.User.Discriminator,
+				instance.Initiator.User.ID)
+			context.SendMessage("Action completed successfully.")
+		},
+	})
+
+	m.Route(&multiplexer.Route{
+		Pattern:       "name",
+		AliasPatterns: []string{"n", "rename"},
+		Description:   "Configure name of a channel.",
+		Category:      system,
+		Handler: func(context *multiplexer.Context) {
+			var name string
+
+			if len(context.Fields) < 2 {
+				// Return if unexpected amount of arguments.
+				context.SendMessage("Expecting new channel name.")
 				return
+			} else {
+				// Trim out first field since it's normally the command prefix or bot mention
+				name = strings.TrimPrefix(
+					strings.TrimPrefix(
+						strings.TrimPrefix(context.Message.Content,
+							context.Multiplexer.Prefix),
+						context.Fields[0]),
+					" ")
+
+				// Check length
+				if len(name) > 32 || len(name) < 2 {
+					context.SendMessage("Name length out of range.")
+					return
+				}
+
+				// Check content
+				if !nameRegex.MatchString(name) {
+					println(context.Fields[0])
+					println(context.Fields[1])
+					context.SendMessage("Name is invalid.")
+					return
+				}
 			}
 
-			// Perform change
-			if channel, err := context.Session.ChannelEditComplex(instance.Channel.ID,
-				&discordgo.ChannelEdit{UserLimit: int(limit), Position: instance.Channel.Position}); !context.HandleError(err) {
+			// Get instance
+			instance, ok := instancesUser[context.User.ID]
+			if !ok {
+				context.SendMessage("You do not own any volatile channel.")
 				return
-			} else {
-				instance.Channel = channel
 			}
 
-			// Register action
-			instance.actionRegister()
+			// Perform action
+			if !instance.actionPerform("name",
+				func() bool {
+					// Check if payload is the same as current state
+					if instance.Channel.Name == name {
+						context.SendMessage("Channel name was not changed.")
+						return false
+					}
 
-			log.Infof("Set user limit of volatile channel #%s (%s) to %v on behalf of %s#%s (%s).",
+					// Perform change
+					if channel, err := context.Session.ChannelEditComplex(instance.Channel.ID,
+						&discordgo.ChannelEdit{Name: name, Position: instance.Channel.Position}); !context.HandleError(err) {
+						return false
+					} else {
+						instance.Channel = channel
+					}
+					return true
+				}, context.SendMessage) {
+				return
+			}
+
+			log.Infof("Set name of volatile channel #%s (%s) to %v on behalf of %s#%s (%s).",
 				instance.Channel.Name,
 				instance.Channel.ID,
-				limit,
+				name,
 				instance.Initiator.User.Username,
 				instance.Initiator.User.Discriminator,
 				instance.Initiator.User.ID)
-			context.SendMessage("Action completed successfully.")
+			context.SendMessage("Action completed.")
 		},
 	})
 }
diff --git a/instance.go b/instance.go
index 137fe9b..3e8b346 100644
--- a/instance.go
+++ b/instance.go
@@ -3,10 +3,13 @@ package main
 import (
 	"git.randomchars.net/freenitori/log"
 	"github.com/bwmarrin/discordgo"
+	"regexp"
 	"sync"
 	"time"
 )
 
+var nameRegex = regexp.MustCompile(`^[A-Za-z0-9()_-]*$`)
+
 var (
 	categoryMap map[string]string
 	guildMap    map[string]string
@@ -16,7 +19,7 @@ var (
 type chatInstance struct {
 	Initiator      *discordgo.Member
 	Channel        *discordgo.Channel
-	PreviousAction time.Time
+	PreviousAction map[string]time.Time
 	Members        map[string]bool
 	sync.RWMutex
 }
@@ -39,13 +42,18 @@ func (c *chatInstance) destroy() {
 }
 
 // actionFree checks if actions are free in chatInstance.
-func (c *chatInstance) actionFree() bool {
-	return c.PreviousAction.Add(timeout).Before(time.Now())
+func (c *chatInstance) actionFree(variant string) bool {
+	p, ok := c.PreviousAction[variant]
+	if !ok {
+		return true
+	}
+
+	return p.Add(timeout).Before(time.Now())
 }
 
 // actionRegister registers an action in chatInstance.
-func (c *chatInstance) actionRegister() {
-	c.PreviousAction = time.Now()
+func (c *chatInstance) actionRegister(variant string) {
+	c.PreviousAction[variant] = time.Now()
 }
 
 // newInstance creates a new chatInstance on behalf of a discordgo.Member.
@@ -90,7 +98,30 @@ func setupInstance(member *discordgo.Member) *chatInstance {
 	return &chatInstance{
 		Initiator:      member,
 		Channel:        channel,
-		PreviousAction: time.Unix(0, 0),
+		PreviousAction: make(map[string]time.Time),
 		Members:        make(map[string]bool),
 	}
 }
+
+// actionPerform performs an action.
+func (c *chatInstance) actionPerform(variant string, action func() bool, sendMessage func(message string) *discordgo.Message) bool {
+	// Lock the instance for write
+	c.Lock()
+	defer c.Unlock()
+
+	// Check if action is free
+	if !c.actionFree(variant) {
+		sendMessage("Not so fast! Please try again later.")
+		return false
+	}
+
+	// Perform action
+	ok := action()
+
+	// Register action
+	if ok {
+		c.actionRegister(variant)
+	}
+
+	return ok
+}
-- 
GitLab