Added /nick and /exit commands.

This commit is contained in:
Andrey Petrov 2014-12-26 14:34:13 -08:00
parent 325f8921da
commit b40136c3e1
5 changed files with 127 additions and 32 deletions

View File

@ -30,7 +30,7 @@ func NewChannel() *Channel {
broadcast: broadcast, broadcast: broadcast,
history: NewHistory(historyLen), history: NewHistory(historyLen),
users: NewSet(), users: NewSet(),
commands: defaultCmdHandlers, commands: *defaultCmdHandlers,
} }
} }
@ -47,15 +47,15 @@ func (ch *Channel) Close() {
} }
// Handle a message, will block until done. // Handle a message, will block until done.
func (ch *Channel) handleMsg(m Message) { func (ch *Channel) HandleMsg(m Message) {
logger.Printf("ch.handleMsg(%v)", m) logger.Printf("ch.HandleMsg(%v)", m)
switch m := m.(type) { switch m := m.(type) {
case *CommandMsg: case *CommandMsg:
cmd := *m cmd := *m
err := ch.commands.Run(ch, cmd) err := ch.commands.Run(ch, cmd)
if err != nil { if err != nil {
m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from) m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from)
go ch.handleMsg(m) go ch.HandleMsg(m)
} }
case MessageTo: case MessageTo:
user := m.To() user := m.To()
@ -86,7 +86,7 @@ func (ch *Channel) handleMsg(m Message) {
// run in a goroutine. // run in a goroutine.
func (ch *Channel) Serve() { func (ch *Channel) Serve() {
for m := range ch.broadcast { for m := range ch.broadcast {
go ch.handleMsg(m) go ch.HandleMsg(m)
} }
} }

View File

@ -2,28 +2,66 @@ package chat
import ( import (
"errors" "errors"
"fmt"
"sort"
"strings" "strings"
"sync"
) )
var ErrInvalidCommand = errors.New("invalid command") var ErrInvalidCommand = errors.New("invalid command")
var ErrNoOwner = errors.New("command without owner") var ErrNoOwner = errors.New("command without owner")
var ErrMissingArg = errors.New("missing argument")
type CommandHandler func(*Channel, CommandMsg) error type CommandHandler func(*Channel, CommandMsg) error
type Commands map[string]CommandHandler type Commands struct {
handlers map[string]CommandHandler
// Register command help map[string]string
func (c Commands) Add(command string, handler CommandHandler) { sync.RWMutex
c[command] = handler
} }
// 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 { func (c Commands) Run(channel *Channel, msg CommandMsg) error {
if msg.from == nil { if msg.from == nil {
return ErrNoOwner return ErrNoOwner
} }
handler, ok := c[msg.Command()] c.RLock()
defer c.RUnlock()
handler, ok := c.handlers[msg.Command()]
if !ok { if !ok {
return ErrInvalidCommand return ErrInvalidCommand
} }
@ -31,12 +69,26 @@ func (c Commands) Run(channel *Channel, msg CommandMsg) error {
return handler(channel, msg) 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() { 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") me := strings.TrimLeft(msg.body, "/me")
if me == "" { if me == "" {
me = " is at a loss for words." me = " is at a loss for words."
@ -48,5 +100,24 @@ func init() {
return nil 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 defaultCmdHandlers = c
} }

View File

@ -10,6 +10,7 @@ import (
type Message interface { type Message interface {
Render(*Theme) string Render(*Theme) string
String() string String() string
Command() string
} }
type MessageTo interface { type MessageTo interface {
@ -50,6 +51,10 @@ func (m *Msg) String() string {
return m.Render(nil) return m.Render(nil)
} }
func (m *Msg) Command() string {
return ""
}
// PublicMsg is any message from a user sent to the channel. // PublicMsg is any message from a user sent to the channel.
type PublicMsg struct { type PublicMsg struct {
Msg Msg

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"io" "io"
"math/rand" "math/rand"
"sync"
"time" "time"
) )
@ -13,14 +14,15 @@ var ErrUserClosed = errors.New("user closed")
// User definition, implemented set Item interface and io.Writer // User definition, implemented set Item interface and io.Writer
type User struct { type User struct {
name string Config UserConfig
op bool name string
colorIdx int op bool
joined time.Time colorIdx int
msg chan Message joined time.Time
done chan struct{} msg chan Message
closed bool done chan struct{}
Config UserConfig closed bool
closeOnce sync.Once
} }
func NewUser(name string) *User { func NewUser(name string) *User {
@ -75,9 +77,11 @@ func (u *User) Wait() {
// Disconnect user, stop accepting messages // Disconnect user, stop accepting messages
func (u *User) Close() { func (u *User) Close() {
u.closed = true u.closeOnce.Do(func() {
close(u.done) u.closed = true
close(u.msg) close(u.done)
close(u.msg)
})
} }
// Consume message buffer into an io.Writer. Will block, should be called in a // 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. // TODO: Not sure if this is a great API.
func (u *User) Consume(out io.Writer) { func (u *User) Consume(out io.Writer) {
for m := range u.msg { for m := range u.msg {
u.handleMsg(m, out) u.HandleMsg(m, out)
} }
} }
// Consume one message and stop, mostly for testing // Consume one message and stop, mostly for testing
func (u *User) ConsumeOne(out io.Writer) { 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) s := m.Render(u.Config.Theme)
_, err := out.Write([]byte(s + Newline)) _, err := out.Write([]byte(s + Newline))
if err != nil { if err != nil {

21
host.go
View File

@ -29,14 +29,22 @@ func NewHost(listener *sshd.SSHListener) *Host {
// Connect a specific Terminal to this host and its channel // Connect a specific Terminal to this host and its channel
func (h *Host) Connect(term *sshd.Terminal) { func (h *Host) Connect(term *sshd.Terminal) {
defer term.Close()
name := term.Conn.User() name := term.Conn.User()
term.SetPrompt(fmt.Sprintf("[%s] ", name))
term.AutoCompleteCallback = h.AutoCompleteFunction term.AutoCompleteCallback = h.AutoCompleteFunction
user := chat.NewUserScreen(name, term) user := chat.NewUserScreen(name, term)
go func() {
// Close term once user is closed.
user.Wait()
term.Close()
}()
defer user.Close() defer user.Close()
refreshPrompt := func() {
term.SetPrompt(fmt.Sprintf("[%s] ", user.Name()))
}
refreshPrompt()
err := h.channel.Join(user) err := h.channel.Join(user)
if err != nil { if err != nil {
logger.Errorf("Failed to join: %s", err) logger.Errorf("Failed to join: %s", err)
@ -54,7 +62,14 @@ func (h *Host) Connect(term *sshd.Terminal) {
break break
} }
m := chat.ParseInput(line, user) 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) err = h.channel.Leave(user)