From b40136c3e128f8ab25a25721bc411d4324aa1dc2 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Fri, 26 Dec 2014 14:34:13 -0800 Subject: [PATCH] Added /nick and /exit commands. --- chat/channel.go | 10 +++--- chat/command.go | 91 +++++++++++++++++++++++++++++++++++++++++++------ chat/message.go | 5 +++ chat/user.go | 32 +++++++++-------- host.go | 21 ++++++++++-- 5 files changed, 127 insertions(+), 32 deletions(-) diff --git a/chat/channel.go b/chat/channel.go index 77eb22c..e7e6f46 100644 --- a/chat/channel.go +++ b/chat/channel.go @@ -30,7 +30,7 @@ func NewChannel() *Channel { broadcast: broadcast, history: NewHistory(historyLen), users: NewSet(), - commands: defaultCmdHandlers, + commands: *defaultCmdHandlers, } } @@ -47,15 +47,15 @@ func (ch *Channel) Close() { } // Handle a message, will block until done. -func (ch *Channel) handleMsg(m Message) { - logger.Printf("ch.handleMsg(%v)", m) +func (ch *Channel) HandleMsg(m Message) { + logger.Printf("ch.HandleMsg(%v)", m) switch m := m.(type) { case *CommandMsg: cmd := *m err := ch.commands.Run(ch, cmd) if err != nil { m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from) - go ch.handleMsg(m) + go ch.HandleMsg(m) } case MessageTo: user := m.To() @@ -86,7 +86,7 @@ func (ch *Channel) handleMsg(m Message) { // run in a goroutine. func (ch *Channel) Serve() { for m := range ch.broadcast { - go ch.handleMsg(m) + go ch.HandleMsg(m) } } diff --git a/chat/command.go b/chat/command.go index 23df6e2..f2b0c46 100644 --- a/chat/command.go +++ b/chat/command.go @@ -2,28 +2,66 @@ package chat import ( "errors" + "fmt" + "sort" "strings" + "sync" ) var ErrInvalidCommand = errors.New("invalid command") var ErrNoOwner = errors.New("command without owner") +var ErrMissingArg = errors.New("missing argument") type CommandHandler func(*Channel, CommandMsg) error -type Commands map[string]CommandHandler - -// Register command -func (c Commands) Add(command string, handler CommandHandler) { - c[command] = handler +type Commands struct { + handlers map[string]CommandHandler + help map[string]string + sync.RWMutex } -// Execute command message, assumes IsCommand was checked +func NewCommands() *Commands { + return &Commands{ + handlers: map[string]CommandHandler{}, + help: map[string]string{}, + } +} + +// Register command. If help string is empty, it will be hidden from Help(). +func (c Commands) Add(command string, help string, handler CommandHandler) { + c.Lock() + defer c.Unlock() + + c.handlers[command] = handler + + if help != "" { + c.help[command] = help + } +} + +// Alias will add another command for the same handler, won't get added to help. +func (c Commands) Alias(command string, alias string) error { + c.Lock() + defer c.Unlock() + + handler, ok := c.handlers[command] + if !ok { + return ErrInvalidCommand + } + c.handlers[alias] = handler + return nil +} + +// Execute command message, assumes IsCommand was checked. func (c Commands) Run(channel *Channel, msg CommandMsg) error { if msg.from == nil { return ErrNoOwner } - handler, ok := c[msg.Command()] + c.RLock() + defer c.RUnlock() + + handler, ok := c.handlers[msg.Command()] if !ok { return ErrInvalidCommand } @@ -31,12 +69,26 @@ func (c Commands) Run(channel *Channel, msg CommandMsg) error { return handler(channel, msg) } -var defaultCmdHandlers Commands +// Help will return collated help text as one string. +func (c Commands) Help() string { + c.RLock() + defer c.RUnlock() + + r := make([]string, len(c.help)) + for cmd, line := range c.help { + r = append(r, fmt.Sprintf("%s %s", cmd, line)) + } + sort.Strings(r) + + return strings.Join(r, Newline) +} + +var defaultCmdHandlers *Commands func init() { - c := Commands{} + c := NewCommands() - c.Add("/me", func(channel *Channel, msg CommandMsg) error { + c.Add("/me", "", func(channel *Channel, msg CommandMsg) error { me := strings.TrimLeft(msg.body, "/me") if me == "" { me = " is at a loss for words." @@ -48,5 +100,24 @@ func init() { return nil }) + c.Add("/exit", "Exit the chat.", func(channel *Channel, msg CommandMsg) error { + msg.From().Close() + return nil + }) + c.Alias("/exit", "/quit") + + c.Add("/nick", "NAME\tRename yourself.", func(channel *Channel, msg CommandMsg) error { + args := msg.Args() + if len(args) != 1 { + return ErrMissingArg + } + u := msg.From() + oldName := u.Name() + u.SetName(args[0]) + + channel.Send(NewAnnounceMsg(fmt.Sprintf("%s is now known as %s.", oldName, u.Name()))) + return nil + }) + defaultCmdHandlers = c } diff --git a/chat/message.go b/chat/message.go index 92be95b..803d2aa 100644 --- a/chat/message.go +++ b/chat/message.go @@ -10,6 +10,7 @@ import ( type Message interface { Render(*Theme) string String() string + Command() string } type MessageTo interface { @@ -50,6 +51,10 @@ func (m *Msg) String() string { return m.Render(nil) } +func (m *Msg) Command() string { + return "" +} + // PublicMsg is any message from a user sent to the channel. type PublicMsg struct { Msg diff --git a/chat/user.go b/chat/user.go index 05ca517..413b4bb 100644 --- a/chat/user.go +++ b/chat/user.go @@ -4,6 +4,7 @@ import ( "errors" "io" "math/rand" + "sync" "time" ) @@ -13,14 +14,15 @@ var ErrUserClosed = errors.New("user closed") // User definition, implemented set Item interface and io.Writer type User struct { - name string - op bool - colorIdx int - joined time.Time - msg chan Message - done chan struct{} - closed bool - Config UserConfig + Config UserConfig + name string + op bool + colorIdx int + joined time.Time + msg chan Message + done chan struct{} + closed bool + closeOnce sync.Once } func NewUser(name string) *User { @@ -75,9 +77,11 @@ func (u *User) Wait() { // Disconnect user, stop accepting messages func (u *User) Close() { - u.closed = true - close(u.done) - close(u.msg) + u.closeOnce.Do(func() { + u.closed = true + close(u.done) + close(u.msg) + }) } // Consume message buffer into an io.Writer. Will block, should be called in a @@ -85,16 +89,16 @@ func (u *User) Close() { // TODO: Not sure if this is a great API. func (u *User) Consume(out io.Writer) { for m := range u.msg { - u.handleMsg(m, out) + u.HandleMsg(m, out) } } // Consume one message and stop, mostly for testing func (u *User) ConsumeOne(out io.Writer) { - u.handleMsg(<-u.msg, out) + u.HandleMsg(<-u.msg, out) } -func (u *User) handleMsg(m Message, out io.Writer) { +func (u *User) HandleMsg(m Message, out io.Writer) { s := m.Render(u.Config.Theme) _, err := out.Write([]byte(s + Newline)) if err != nil { diff --git a/host.go b/host.go index 4b72a77..6325c20 100644 --- a/host.go +++ b/host.go @@ -29,14 +29,22 @@ func NewHost(listener *sshd.SSHListener) *Host { // Connect a specific Terminal to this host and its channel func (h *Host) Connect(term *sshd.Terminal) { - defer term.Close() name := term.Conn.User() - term.SetPrompt(fmt.Sprintf("[%s] ", name)) term.AutoCompleteCallback = h.AutoCompleteFunction user := chat.NewUserScreen(name, term) + go func() { + // Close term once user is closed. + user.Wait() + term.Close() + }() defer user.Close() + refreshPrompt := func() { + term.SetPrompt(fmt.Sprintf("[%s] ", user.Name())) + } + refreshPrompt() + err := h.channel.Join(user) if err != nil { logger.Errorf("Failed to join: %s", err) @@ -54,7 +62,14 @@ func (h *Host) Connect(term *sshd.Terminal) { break } m := chat.ParseInput(line, user) - h.channel.Send(m) + // FIXME: Any reason to use h.channel.Send(m) instead? + h.channel.HandleMsg(m) + if m.Command() == "/nick" { + // Hijack /nick command to update terminal synchronously. Wouldn't + // work if we use h.channel.Send(m) above. + // FIXME: This is hacky, how do we improve the API to allow for this? + refreshPrompt() + } } err = h.channel.Leave(user)