From 1652511bf239a6c8c32c1654df7b5ac2aea45711 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Sat, 20 Dec 2014 20:21:41 -0800 Subject: [PATCH] Progress, most of this probably doesnt work. --- chat/channel.go | 32 +++++++++++++++++---------- chat/channel_test.go | 27 +++++++++++++++++++++++ chat/command.go | 52 ++++++++++++++++++++++++++++++++++++++++++++ chat/host.go | 43 ++++++++++++++++++++++++++++++++++++ chat/message.go | 26 +++++++++++++++++----- chat/set_test.go | 4 ++-- chat/user.go | 17 ++++++++++++--- chat/user_test.go | 26 ++++++++++++++++++++++ 8 files changed, 206 insertions(+), 21 deletions(-) create mode 100644 chat/channel_test.go create mode 100644 chat/command.go create mode 100644 chat/host.go create mode 100644 chat/user_test.go diff --git a/chat/channel.go b/chat/channel.go index b478c82..d6b1393 100644 --- a/chat/channel.go +++ b/chat/channel.go @@ -1,37 +1,47 @@ package chat +import "fmt" + const historyLen = 20 // Channel definition, also a Set of User Items type Channel struct { - id string - topic string - history *History - users *Set - out chan<- Message + id string + topic string + history *History + users *Set + broadcast chan<- Message } -func NewChannel(id string, out chan<- Message) *Channel { +func NewChannel(id string, broadcast chan<- Message) *Channel { ch := Channel{ - id: id, - out: out, - history: NewHistory(historyLen), - users: NewSet(), + id: id, + broadcast: broadcast, + history: NewHistory(historyLen), + users: NewSet(), } return &ch } func (ch *Channel) Send(m Message) { - ch.out <- m + ch.broadcast <- m } func (ch *Channel) Join(u *User) error { err := ch.users.Add(u) + if err != nil { + s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), ch.users.Len()) + ch.Send(*NewMessage(s)) + } return err } func (ch *Channel) Leave(u *User) error { err := ch.users.Remove(u) + if err != nil { + s := fmt.Sprintf("%s left.", u.Name()) + ch.Send(*NewMessage(s)) + } return err } diff --git a/chat/channel_test.go b/chat/channel_test.go new file mode 100644 index 0000000..53debc9 --- /dev/null +++ b/chat/channel_test.go @@ -0,0 +1,27 @@ +package chat + +import ( + "reflect" + "testing" +) + +func TestChannel(t *testing.T) { + s := &MockScreen{} + out := make(chan Message) + defer close(out) + + go func() { + for msg := range out { + s.Write([]byte(msg.Render(nil))) + } + }() + + u := NewUser("foo", s) + ch := NewChannel("", out) + ch.Join(u) + + expected := []byte(" * foo joined. (Connected: 1)") + if !reflect.DeepEqual(s.received, expected) { + t.Errorf("Got: %s, Expected: %s", s.received, expected) + } +} diff --git a/chat/command.go b/chat/command.go new file mode 100644 index 0000000..73ffa2f --- /dev/null +++ b/chat/command.go @@ -0,0 +1,52 @@ +package chat + +import ( + "errors" + "strings" +) + +var ErrInvalidCommand error = errors.New("invalid command") +var ErrNoOwner error = errors.New("command without owner") + +type CmdHandler func(host Host, msg Message, args []string) error + +type Commands map[string]CmdHandler + +// Register command +func (c Commands) Add(cmd string, handler CmdHandler) { + c[cmd] = handler +} + +// Execute command message, assumes IsCommand was checked +func (c Commands) Run(host Host, msg Message) error { + if msg.from == nil { + return ErrNoOwner + } + + cmd, args := msg.ParseCommand() + handler, ok := c[cmd] + if !ok { + return ErrInvalidCommand + } + + return handler(host, msg, args) +} + +var defaultCmdHandlers Commands + +func init() { + c := Commands{} + + c.Add("me", func(host Host, msg Message, args []string) error { + me := strings.TrimLeft(msg.Body, "/me") + if me == "" { + me = " is at a loss for words." + } + + // XXX: Finish this. + + return nil + }) + + defaultCmdHandlers = c +} diff --git a/chat/host.go b/chat/host.go new file mode 100644 index 0000000..bb20571 --- /dev/null +++ b/chat/host.go @@ -0,0 +1,43 @@ +package chat + +const messageBuffer = 20 + +// Host knows about all the commands and channels. +type Host struct { + defaultChannel *Channel + commands Commands + done chan struct{} +} + +func NewHost() *Host { + h := Host{ + commands: defaultCmdHandlers, + } + h.defaultChannel = h.CreateChannel("") + return &h +} + +func (h *Host) handleCommand(m Message) { + // TODO: ... +} + +func (h *Host) broadcast(ch *Channel, m Message) { + // TODO: ... +} + +func (h *Host) CreateChannel(id string) *Channel { + out := make(chan Message, 20) + ch := NewChannel(id, out) + + go func() { + for msg := range out { + if msg.IsCommand() { + go h.handleCommand(msg) + continue + } + h.broadcast(ch, msg) + } + }() + + return ch +} diff --git a/chat/message.go b/chat/message.go index 6050e83..04d816f 100644 --- a/chat/message.go +++ b/chat/message.go @@ -2,6 +2,7 @@ package chat import ( "fmt" + "strings" "time" ) @@ -14,10 +15,9 @@ type Message struct { themeCache *map[*Theme]string } -func NewMessage(from *User, body string) *Message { +func NewMessage(body string) *Message { m := Message{ Body: body, - from: from, timestamp: time.Now(), } return &m @@ -37,19 +37,35 @@ func (m *Message) From(u *User) *Message { // Render message based on the given theme func (m *Message) Render(*Theme) string { - // TODO: Render based on theme. + // TODO: Render based on theme // TODO: Cache based on theme var msg string - if m.to != nil { - msg = fmt.Sprintf("[PM from %s] %s", m.to, m.Body) + if m.to != nil && m.from != nil { + msg = fmt.Sprintf("[PM from %s] %s", m.from, m.Body) } else if m.from != nil { msg = fmt.Sprintf("%s: %s", m.from, m.Body) + } else if m.to != nil { + msg = fmt.Sprintf("-> %s", m.Body) } else { msg = fmt.Sprintf(" * %s", m.Body) } return msg } +// Render message without a theme func (m *Message) String() string { return m.Render(nil) } + +// Wether message is a command (starts with /) +func (m *Message) IsCommand() bool { + return strings.HasPrefix(m.Body, "/") +} + +// 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 +} diff --git a/chat/set_test.go b/chat/set_test.go index 60038c1..b55972b 100644 --- a/chat/set_test.go +++ b/chat/set_test.go @@ -5,7 +5,7 @@ import "testing" func TestSet(t *testing.T) { var err error s := NewSet() - u := NewUser("foo") + u := NewUser("foo", nil) if s.In(u) { t.Errorf("Set should be empty.") @@ -20,7 +20,7 @@ func TestSet(t *testing.T) { t.Errorf("Set should contain user.") } - u2 := NewUser("bar") + u2 := NewUser("bar", nil) err = s.Add(u2) if err != nil { t.Error(err) diff --git a/chat/user.go b/chat/user.go index 97acb8b..946c371 100644 --- a/chat/user.go +++ b/chat/user.go @@ -1,21 +1,27 @@ package chat import ( + "io" "math/rand" "time" ) -// User definition, implemented set Item interface +// User definition, implemented set Item interface and io.Writer type User struct { name string op bool colorIdx int joined time.Time + screen io.Writer Config UserConfig } -func NewUser(name string) *User { - u := User{Config: *DefaultUserConfig} +func NewUser(name string, screen io.Writer) *User { + u := User{ + screen: screen, + joined: time.Now(), + Config: *DefaultUserConfig, + } u.SetName(name) return &u } @@ -46,6 +52,11 @@ func (u *User) SetOp(op bool) { u.op = op } +// Write to user's screen +func (u *User) Write(p []byte) (n int, err error) { + return u.screen.Write(p) +} + // Container for per-user configurations. type UserConfig struct { Highlight bool diff --git a/chat/user_test.go b/chat/user_test.go new file mode 100644 index 0000000..1f0c5cd --- /dev/null +++ b/chat/user_test.go @@ -0,0 +1,26 @@ +package chat + +import ( + "reflect" + "testing" +) + +type MockScreen struct { + received []byte +} + +func (s *MockScreen) Write(data []byte) (n int, err error) { + s.received = append(s.received, data...) + return len(data), nil +} + +func TestMakeUser(t *testing.T) { + s := &MockScreen{} + u := NewUser("foo", s) + + line := []byte("hello") + u.Write(line) + if !reflect.DeepEqual(s.received, line) { + t.Errorf("Expected hello but got: %s", s.received) + } +}