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,
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)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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 {

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
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)