From 36843252b4f059161f36cd9fa54284fb8e39e0c0 Mon Sep 17 00:00:00 2001
From: Andrey Petrov <andrey.petrov@shazow.net>
Date: Wed, 24 Jun 2020 12:36:02 -0400
Subject: [PATCH] chat, main: Add /rename op command

---
 chat/message/user.go | 10 ++++++++
 host.go              | 56 +++++++++++++++++++++++++++++++++++---------
 2 files changed, 55 insertions(+), 11 deletions(-)

diff --git a/chat/message/user.go b/chat/message/user.go
index 0fc8cc1..1d1a4f0 100644
--- a/chat/message/user.go
+++ b/chat/message/user.go
@@ -23,6 +23,8 @@ var ErrUserClosed = errors.New("user closed")
 type User struct {
 	Identifier
 	Ignored  *set.Set
+	OnChange func()
+
 	colorIdx int
 	joined   time.Time
 	msg      chan Message
@@ -72,12 +74,20 @@ func (u *User) SetConfig(cfg UserConfig) {
 	u.mu.Lock()
 	u.config = cfg
 	u.mu.Unlock()
+
+	if u.OnChange != nil {
+		u.OnChange()
+	}
 }
 
 // Rename the user with a new Identifier.
 func (u *User) SetID(id string) {
 	u.Identifier.SetID(id)
 	u.setColorIdx(rand.Int())
+
+	if u.OnChange != nil {
+		u.OnChange()
+	}
 }
 
 // ReplyTo returns the last user that messaged this user.
diff --git a/host.go b/host.go
index 8973229..a943ff6 100644
--- a/host.go
+++ b/host.go
@@ -13,6 +13,7 @@ import (
 	"github.com/shazow/ssh-chat/chat"
 	"github.com/shazow/ssh-chat/chat/message"
 	"github.com/shazow/ssh-chat/internal/humantime"
+	"github.com/shazow/ssh-chat/internal/sanitize"
 	"github.com/shazow/ssh-chat/sshd"
 )
 
@@ -92,6 +93,10 @@ func (h *Host) isOp(conn sshd.Connection) bool {
 func (h *Host) Connect(term *sshd.Terminal) {
 	id := NewIdentity(term.Conn)
 	user := message.NewUserScreen(id, term)
+	user.OnChange = func() {
+		term.SetPrompt(GetPrompt(user))
+		user.SetHighlight(user.Name())
+	}
 	cfg := user.Config()
 
 	apiMode := strings.ToLower(term.Term()) == "bot"
@@ -216,17 +221,6 @@ func (h *Host) Connect(term *sshd.Terminal) {
 			// Skip the remaining rendering workarounds
 			continue
 		}
-
-		cmd := m.Command()
-		if cmd == "/nick" || cmd == "/theme" {
-			// Hijack /nick command to update terminal synchronously. Wouldn't
-			// work if we use h.room.Send(m) above.
-			//
-			// FIXME: This is hacky, how do we improve the API to allow for
-			// this? Chat module shouldn't know about terminals.
-			term.SetPrompt(GetPrompt(user))
-			user.SetHighlight(user.Name())
-		}
 	}
 
 	err = h.Leave(user)
@@ -619,4 +613,44 @@ func (h *Host) InitCommands(c *chat.Commands) {
 			return nil
 		},
 	})
+
+	c.Add(chat.Command{
+		Op:         true,
+		Prefix:     "/rename",
+		PrefixHelp: "USER NEW_NAME [SYMBOL]",
+		Help:       "Rename USER to NEW_NAME, add optional SYMBOL prefix",
+		Handler: func(room *chat.Room, msg message.CommandMsg) error {
+			if !room.IsOp(msg.From()) {
+				return errors.New("must be op")
+			}
+
+			args := msg.Args()
+			if len(args) < 2 {
+				return errors.New("must specify user and new name")
+			}
+
+			member, ok := room.MemberByID(args[0])
+			if !ok {
+				return errors.New("user not found")
+			}
+
+			oldID := member.ID()
+			newID := sanitize.Name(args[1])
+			if newID == oldID {
+				return errors.New("new name is the same as the original")
+			}
+
+			member.SetID(newID)
+			err := room.Rename(oldID, member)
+			if err != nil {
+				member.SetID(oldID)
+				return err
+			}
+
+			body := fmt.Sprintf("%s was renamed by %s.", oldID, msg.From().Name())
+			room.Send(message.NewAnnounceMsg(body))
+
+			return nil
+		},
+	})
 }