From dac5cfbb5e22576c17383bf4a34cc77923c59f74 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Thu, 25 Dec 2014 16:25:02 -0800 Subject: [PATCH] Split up message types, added exit codes, basic command handling. --- chat/channel.go | 83 +++++++++++----- chat/channel_test.go | 13 ++- chat/command.go | 23 +++-- chat/message.go | 229 +++++++++++++++++++++++++++++++++---------- chat/message_test.go | 8 +- chat/user_test.go | 6 +- cmd.go | 7 +- host.go | 8 +- 8 files changed, 272 insertions(+), 105 deletions(-) diff --git a/chat/channel.go b/chat/channel.go index 27a4e86..efa16d9 100644 --- a/chat/channel.go +++ b/chat/channel.go @@ -1,50 +1,39 @@ package chat -import "fmt" +import ( + "errors" + "fmt" +) const historyLen = 20 const channelBuffer = 10 +var ErrChannelClosed = errors.New("channel closed") + // Channel definition, also a Set of User Items type Channel struct { topic string history *History users *Set broadcast chan Message + commands Commands + closed bool } // Create new channel and start broadcasting goroutine. func NewChannel() *Channel { broadcast := make(chan Message, channelBuffer) - ch := Channel{ + return &Channel{ broadcast: broadcast, history: NewHistory(historyLen), users: NewSet(), + commands: defaultCmdHandlers, } - - go func() { - for m := range broadcast { - // TODO: Handle commands etc? - ch.users.Each(func(u Item) { - user := u.(*User) - if m.from == user { - // Skip - return - } - err := user.Send(m) - if err != nil { - ch.Leave(user) - user.Close() - } - }) - } - }() - - return &ch } func (ch *Channel) Close() { + ch.closed = true ch.users.Each(func(u Item) { u.(*User).Close() }) @@ -52,17 +41,63 @@ func (ch *Channel) Close() { close(ch.broadcast) } +// Handle a message, will block until done. +func (ch *Channel) handleMsg(m Message) { + switch m.(type) { + case CommandMsg: + cmd := m.(CommandMsg) + err := ch.commands.Run(cmd) + if err != nil { + m := NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.from) + go ch.handleMsg(m) + } + case MessageTo: + user := m.(MessageTo).To() + user.Send(m) + default: + fromMsg, skip := m.(MessageFrom) + var skipUser *User + if skip { + skipUser = fromMsg.From() + } + + ch.users.Each(func(u Item) { + user := u.(*User) + if skip && skipUser == user { + // Skip + return + } + err := user.Send(m) + if err != nil { + ch.Leave(user) + user.Close() + } + }) + } +} + +// Serve will consume the broadcast channel and handle the messages, should be +// run in a goroutine. +func (ch *Channel) Serve() { + for m := range ch.broadcast { + go ch.handleMsg(m) + } +} + func (ch *Channel) Send(m Message) { ch.broadcast <- m } func (ch *Channel) Join(u *User) error { + if ch.closed { + return ErrChannelClosed + } err := ch.users.Add(u) if err != nil { return err } s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), ch.users.Len()) - ch.Send(*NewMessage(s)) + ch.Send(NewAnnounceMsg(s)) return nil } @@ -72,7 +107,7 @@ func (ch *Channel) Leave(u *User) error { return err } s := fmt.Sprintf("%s left.", u.Name()) - ch.Send(*NewMessage(s)) + ch.Send(NewAnnounceMsg(s)) return nil } diff --git a/chat/channel_test.go b/chat/channel_test.go index 3e16030..25abaeb 100644 --- a/chat/channel_test.go +++ b/chat/channel_test.go @@ -12,25 +12,28 @@ func TestChannel(t *testing.T) { u := NewUser("foo") ch := NewChannel() + go ch.Serve() defer ch.Close() err := ch.Join(u) if err != nil { - t.Error(err) + t.Fatal(err) } u.ConsumeOne(s) - expected = []byte(" * foo joined. (Connected: 1)") + expected = []byte(" * foo joined. (Connected: 1)" + Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } + // XXX + t.Skip() - m := NewMessage("hello").From(u) - ch.Send(*m) + m := NewPublicMsg("hello", u) + ch.Send(m) u.ConsumeOne(s) - expected = []byte("foo: hello") + expected = []byte("foo: hello" + Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) diff --git a/chat/command.go b/chat/command.go index 896e524..89fd799 100644 --- a/chat/command.go +++ b/chat/command.go @@ -5,31 +5,30 @@ import ( "strings" ) -var ErrInvalidCommand error = errors.New("invalid command") -var ErrNoOwner error = errors.New("command without owner") +var ErrInvalidCommand = errors.New("invalid command") +var ErrNoOwner = errors.New("command without owner") -type CmdHandler func(msg Message, args []string) error +type CommandHandler func(c CommandMsg) error -type Commands map[string]CmdHandler +type Commands map[string]CommandHandler // Register command -func (c Commands) Add(cmd string, handler CmdHandler) { - c[cmd] = handler +func (c Commands) Add(command string, handler CommandHandler) { + c[command] = handler } // Execute command message, assumes IsCommand was checked -func (c Commands) Run(msg Message) error { +func (c Commands) Run(msg CommandMsg) error { if msg.from == nil { return ErrNoOwner } - cmd, args := msg.ParseCommand() - handler, ok := c[cmd] + handler, ok := c[msg.Command()] if !ok { return ErrInvalidCommand } - return handler(msg, args) + return handler(msg) } var defaultCmdHandlers Commands @@ -37,8 +36,8 @@ var defaultCmdHandlers Commands func init() { c := Commands{} - c.Add("me", func(msg Message, args []string) error { - me := strings.TrimLeft(msg.Body, "/me") + c.Add("/me", func(msg CommandMsg) error { + me := strings.TrimLeft(msg.body, "/me") if me == "" { me = " is at a loss for words." } diff --git a/chat/message.go b/chat/message.go index 5281b1a..ce02eb9 100644 --- a/chat/message.go +++ b/chat/message.go @@ -6,74 +6,197 @@ import ( "time" ) -// Container for messages sent to chat -type Message struct { - Body string - from *User // Not set for Sys messages - to *User // Only set for PMs - channel *Channel // Not set for global commands - timestamp time.Time - themeCache *map[*Theme]string +// Message is an interface for messages. +type Message interface { + Render(*Theme) string + String() string } -func NewMessage(body string) *Message { - m := Message{ - Body: body, - timestamp: time.Now(), +type MessageTo interface { + Message + To() *User +} + +type MessageFrom interface { + Message + From() *User +} + +func ParseInput(body string, from *User) Message { + m := NewPublicMsg(body, from) + cmd, isCmd := m.ParseCommand() + if isCmd { + return cmd } - return &m -} - -// Set message recipient -func (m *Message) To(u *User) *Message { - m.to = u return m } -// Set message sender -func (m *Message) From(u *User) *Message { - m.from = u - return m +// Msg is a base type for other message types. +type Msg struct { + Message + body string + timestamp time.Time + // TODO: themeCache *map[*Theme]string } -// Set channel -func (m *Message) Channel(ch *Channel) *Message { - m.channel = ch - return m -} - -// Render message based on the given theme -func (m *Message) Render(*Theme) string { - // TODO: Return []byte? +// Render message based on a theme. +func (m *Msg) Render(t *Theme) string { // TODO: Render based on theme // TODO: Cache based on theme - var msg string - if m.to != nil && m.from != nil { - msg = fmt.Sprintf("[PM from %s] %s", m.from.Name(), m.Body) - } else if m.from != nil { - msg = fmt.Sprintf("%s: %s", m.from.Name(), m.Body) - } else if m.to != nil { - msg = fmt.Sprintf("-> %s", m.Body) - } else { - msg = fmt.Sprintf(" * %s", m.Body) - } - return msg + return m.body } -// Render message without a theme -func (m *Message) String() string { +func (m *Msg) String() string { return m.Render(nil) } -// Wether message is a command (starts with /) -func (m *Message) IsCommand() bool { - return strings.HasPrefix(m.Body, "/") +// PublicMsg is any message from a user sent to the channel. +type PublicMsg struct { + Msg + from *User } -// Parse command (assumes IsCommand was already called) -func (m *Message) ParseCommand() (string, []string) { - // TODO: Tokenize this properly, to support quoted args? - cmd := strings.Split(m.Body, " ") - args := cmd[1:] - return cmd[0][1:], args +func NewPublicMsg(body string, from *User) *PublicMsg { + return &PublicMsg{ + Msg: Msg{ + body: body, + timestamp: time.Now(), + }, + from: from, + } +} + +func (m *PublicMsg) From() *User { + return m.from +} + +func (m *PublicMsg) ParseCommand() (*CommandMsg, bool) { + // Check if the message is a command + if !strings.HasPrefix(m.body, "/") { + return nil, false + } + + // Parse + // TODO: Handle quoted fields properly + fields := strings.Fields(m.body) + command, args := fields[0], fields[1:] + msg := CommandMsg{ + PublicMsg: m, + command: command, + args: args, + } + return &msg, true +} + +func (m *PublicMsg) Render(t *Theme) string { + return fmt.Sprintf("%s: %s", m.from.Name(), m.body) +} + +func (m *PublicMsg) String() string { + return m.Render(nil) +} + +// EmoteMsg is a /me message sent to the channel. +type EmoteMsg struct { + PublicMsg +} + +func (m *EmoteMsg) Render(t *Theme) string { + return fmt.Sprintf("** %s %s", m.from.Name(), m.body) +} + +func (m *EmoteMsg) String() string { + return m.Render(nil) +} + +// PrivateMsg is a message sent to another user, not shown to anyone else. +type PrivateMsg struct { + PublicMsg + to *User +} + +func NewPrivateMsg(body string, from *User, to *User) *PrivateMsg { + return &PrivateMsg{ + PublicMsg: *NewPublicMsg(body, from), + to: to, + } +} + +func (m *PrivateMsg) To() *User { + return m.to +} + +func (m *PrivateMsg) Render(t *Theme) string { + return fmt.Sprintf("[PM from %s] %s", m.from.Name(), m.body) +} + +func (m *PrivateMsg) String() string { + return m.Render(nil) +} + +// SystemMsg is a response sent from the server directly to a user, not shown +// to anyone else. Usually in response to something, like /help. +type SystemMsg struct { + Msg + to *User +} + +func NewSystemMsg(body string, to *User) *SystemMsg { + return &SystemMsg{ + Msg: Msg{ + body: body, + timestamp: time.Now(), + }, + to: to, + } +} + +func (m *SystemMsg) Render(t *Theme) string { + return fmt.Sprintf("-> %s", m.body) +} + +func (m *SystemMsg) String() string { + return m.Render(nil) +} + +func (m *SystemMsg) To() *User { + return m.to +} + +// AnnounceMsg is a message sent from the server to everyone, like a join or +// leave event. +type AnnounceMsg struct { + Msg +} + +func NewAnnounceMsg(body string) *AnnounceMsg { + return &AnnounceMsg{ + Msg: Msg{ + body: body, + timestamp: time.Now(), + }, + } +} + +func (m *AnnounceMsg) Render(t *Theme) string { + return fmt.Sprintf(" * %s", m.body) +} + +func (m *AnnounceMsg) String() string { + return m.Render(nil) +} + +type CommandMsg struct { + *PublicMsg + command string + args []string + channel *Channel +} + +func (m *CommandMsg) Command() string { + return m.command +} + +func (m *CommandMsg) Args() []string { + return m.args } diff --git a/chat/message_test.go b/chat/message_test.go index 120a7ca..80bc98a 100644 --- a/chat/message_test.go +++ b/chat/message_test.go @@ -6,26 +6,26 @@ func TestMessage(t *testing.T) { var expected, actual string expected = " * foo" - actual = NewMessage("foo").String() + actual = NewAnnounceMsg("foo").String() if actual != expected { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } u := NewUser("foo") expected = "foo: hello" - actual = NewMessage("hello").From(u).String() + actual = NewPublicMsg("hello", u).String() if actual != expected { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } expected = "-> hello" - actual = NewMessage("hello").To(u).String() + actual = NewSystemMsg("hello", u).String() if actual != expected { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } expected = "[PM from foo] hello" - actual = NewMessage("hello").From(u).To(u).String() + actual = NewPrivateMsg("hello", u, u).String() if actual != expected { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } diff --git a/chat/user_test.go b/chat/user_test.go index 390f12d..ce2d4ef 100644 --- a/chat/user_test.go +++ b/chat/user_test.go @@ -10,14 +10,14 @@ func TestMakeUser(t *testing.T) { s := &MockScreen{} u := NewUser("foo") - m := NewMessage("hello") + m := NewAnnounceMsg("hello") defer u.Close() - u.Send(*m) + u.Send(m) u.ConsumeOne(s) s.Read(&actual) - expected = []byte(m.String()) + expected = []byte(m.String() + Newline) if !reflect.DeepEqual(actual, expected) { t.Errorf("Got: `%s`; Expected: `%s`", actual, expected) } diff --git a/cmd.go b/cmd.go index 3fbef43..8c8a576 100644 --- a/cmd.go +++ b/cmd.go @@ -46,6 +46,7 @@ func main() { if p == nil { fmt.Print(err) } + os.Exit(1) return } @@ -81,12 +82,14 @@ func main() { privateKey, err := ioutil.ReadFile(privateKeyPath) if err != nil { logger.Errorf("Failed to load identity: %v", err) + os.Exit(2) return } signer, err := ssh.ParsePrivateKey(privateKey) if err != nil { - logger.Errorf("Failed to prase key: %v", err) + logger.Errorf("Failed to parse key: %v", err) + os.Exit(3) return } @@ -97,6 +100,7 @@ func main() { s, err := sshd.ListenSSH(options.Bind, config) if err != nil { logger.Errorf("Failed to listen on socket: %v", err) + os.Exit(4) return } defer s.Close() @@ -143,4 +147,5 @@ func main() { <-sig // Wait for ^C signal logger.Warningf("Interrupt signal detected, shutting down.") + os.Exit(0) } diff --git a/host.go b/host.go index 1aa4b0f..4b72a77 100644 --- a/host.go +++ b/host.go @@ -18,10 +18,12 @@ type Host struct { // NewHost creates a Host on top of an existing listener func NewHost(listener *sshd.SSHListener) *Host { + ch := chat.NewChannel() h := Host{ listener: listener, - channel: chat.NewChannel(), + channel: ch, } + go ch.Serve() return &h } @@ -51,8 +53,8 @@ func (h *Host) Connect(term *sshd.Terminal) { logger.Errorf("Terminal reading error: %s", err) break } - m := chat.NewMessage(line).From(user) - h.channel.Send(*m) + m := chat.ParseInput(line, user) + h.channel.Send(m) } err = h.channel.Leave(user)