diff --git a/chat/message/config.go b/chat/message/config.go new file mode 100644 index 0000000..9cb8af2 --- /dev/null +++ b/chat/message/config.go @@ -0,0 +1,24 @@ +package message + +import "regexp" + +// Container for per-user configurations. +type UserConfig struct { + Highlight *regexp.Regexp + Bell bool + Quiet bool + Theme *Theme + Seed int +} + +// Default user configuration to use +var DefaultUserConfig UserConfig + +func init() { + DefaultUserConfig = UserConfig{ + Bell: true, + Quiet: false, + } + + // TODO: Seed random? +} diff --git a/chat/message/screen.go b/chat/message/screen.go new file mode 100644 index 0000000..c26bd3b --- /dev/null +++ b/chat/message/screen.go @@ -0,0 +1,206 @@ +package message + +import ( + "errors" + "fmt" + "io" + "math/rand" + "regexp" + "sync" + "time" +) + +var ErrUserClosed = errors.New("user closed") + +const messageBuffer = 5 +const messageTimeout = 5 * time.Second + +func BufferedScreen(name string, screen io.WriteCloser) *bufferedScreen { + return &bufferedScreen{ + pipedScreen: PipedScreen(name, screen), + msg: make(chan Message, messageBuffer), + done: make(chan struct{}), + } +} + +func PipedScreen(name string, screen io.WriteCloser) *pipedScreen { + return &pipedScreen{ + baseScreen: &baseScreen{ + User: NewUser(name), + }, + WriteCloser: screen, + } +} + +func HandledScreen(name string, handler func(Message) error) *handledScreen { + return &handledScreen{ + baseScreen: &baseScreen{ + User: NewUser(name), + }, + handler: handler, + } +} + +type handledScreen struct { + *baseScreen + handler func(Message) error +} + +func (u *handledScreen) Send(m Message) error { + return u.handler(m) +} + +// Screen that pipes messages to an io.WriteCloser +type pipedScreen struct { + *baseScreen + io.WriteCloser +} + +func (u *pipedScreen) Send(m Message) error { + r := u.render(m) + _, err := u.Write([]byte(r)) + if err != nil { + logger.Printf("Write failed to %s, closing: %s", u.Name(), err) + u.Close() + } + return err +} + +// User container that knows about writing to an IO screen. +type baseScreen struct { + sync.Mutex + *User +} + +func (u *baseScreen) Config() UserConfig { + u.Lock() + defer u.Unlock() + return u.config +} + +func (u *baseScreen) SetConfig(cfg UserConfig) { + u.Lock() + u.config = cfg + u.Unlock() +} + +func (u *baseScreen) ID() string { + u.Lock() + defer u.Unlock() + return SanitizeName(u.name) +} + +func (u *baseScreen) Name() string { + u.Lock() + defer u.Unlock() + return u.name +} + +func (u *baseScreen) Joined() time.Time { + return u.joined +} + +// Rename the user with a new Identifier. +func (u *baseScreen) SetName(name string) { + u.Lock() + u.name = name + u.config.Seed = rand.Int() + u.Unlock() +} + +// ReplyTo returns the last user that messaged this user. +func (u *baseScreen) ReplyTo() Author { + u.Lock() + defer u.Unlock() + return u.replyTo +} + +// SetReplyTo sets the last user to message this user. +func (u *baseScreen) SetReplyTo(user Author) { + // TODO: Use UserConfig.ReplyTo string + u.Lock() + defer u.Unlock() + u.replyTo = user +} + +// SetHighlight sets the highlighting regular expression to match string. +func (u *baseScreen) SetHighlight(s string) error { + re, err := regexp.Compile(fmt.Sprintf(reHighlight, s)) + if err != nil { + return err + } + u.Lock() + u.config.Highlight = re + u.Unlock() + return nil +} + +func (u *baseScreen) render(m Message) string { + cfg := u.Config() + switch m := m.(type) { + case PublicMsg: + return m.RenderFor(cfg) + Newline + case PrivateMsg: + u.SetReplyTo(m.From()) + return m.Render(cfg.Theme) + Newline + default: + return m.Render(cfg.Theme) + Newline + } +} + +// Prompt renders a theme-colorized prompt string. +func (u *baseScreen) Prompt() string { + name := u.Name() + cfg := u.Config() + if cfg.Theme != nil { + name = cfg.Theme.ColorName(u) + } + return fmt.Sprintf("[%s] ", name) +} + +// bufferedScreen is a screen that buffers messages on Send using a channel and a consuming goroutine. +type bufferedScreen struct { + *pipedScreen + closeOnce sync.Once + msg chan Message + done chan struct{} +} + +func (u *bufferedScreen) Close() error { + u.closeOnce.Do(func() { + close(u.done) + }) + + return u.pipedScreen.Close() +} + +// Add message to consume by user +func (u *bufferedScreen) Send(m Message) error { + select { + case <-u.done: + return ErrUserClosed + case u.msg <- m: + case <-time.After(messageTimeout): + logger.Printf("Message buffer full, closing: %s", u.Name()) + u.Close() + return ErrUserClosed + } + return nil +} + +// Consume message buffer into the handler. Will block, should be called in a +// goroutine. +func (u *bufferedScreen) Consume() { + for { + select { + case <-u.done: + return + case m, ok := <-u.msg: + if !ok { + return + } + // Pass on to unbuffered screen. + u.pipedScreen.Send(m) + } + } +} diff --git a/chat/message/theme_test.go b/chat/message/theme_test.go index d3d4577..c16222f 100644 --- a/chat/message/theme_test.go +++ b/chat/message/theme_test.go @@ -52,7 +52,7 @@ func TestTheme(t *testing.T) { } u := NewUser("foo") - u.colorIdx = 4 + u.config.Seed = 4 actual = colorTheme.ColorName(u) expected = "\033[38;05;5mfoo\033[0m" if actual != expected { diff --git a/chat/message/user.go b/chat/message/user.go index 7c9c2ac..022f577 100644 --- a/chat/message/user.go +++ b/chat/message/user.go @@ -1,75 +1,16 @@ package message import ( - "errors" - "fmt" - "io" "math/rand" - "regexp" - "sync" "time" ) -const messageBuffer = 5 -const messageTimeout = 5 * time.Second const reHighlight = `\b(%s)\b` -var ErrUserClosed = errors.New("user closed") - -// User container that knows about writing to an IO screen. -type UserScreen struct { - *User - io.WriteCloser -} - -func (u *UserScreen) Close() error { - u.User.Close() - return u.WriteCloser.Close() -} - -// HandleMsg will render the message to the screen, blocking. -func (u *UserScreen) HandleMsg(m Message) error { - r := u.render(m) - _, err := u.Write([]byte(r)) - if err != nil { - logger.Printf("Write failed to %s, closing: %s", u.Name(), err) - u.User.Close() - u.WriteCloser.Close() - } - return err -} - -// Consume message buffer into the handler. Will block, should be called in a -// goroutine. -func (u *UserScreen) Consume() { - for { - select { - case <-u.done: - return - case m, ok := <-u.msg: - if !ok { - return - } - u.HandleMsg(m) - } - } -} - -// Consume one message and stop, mostly for testing -// TODO: Stop using it and remove it. -func (u *UserScreen) ConsumeOne() Message { - return <-u.msg -} - // User definition, implemented set Item interface and io.Writer type User struct { - colorIdx int - joined time.Time - closeOnce sync.Once - msg chan Message - done chan struct{} + joined time.Time - mu sync.Mutex name string config UserConfig replyTo Author // Set when user gets a /msg, for replying. @@ -80,155 +21,20 @@ func NewUser(name string) *User { name: name, config: DefaultUserConfig, joined: time.Now(), - msg: make(chan Message, messageBuffer), - done: make(chan struct{}), } - u.setColorIdx(rand.Int()) + u.config.Seed = rand.Int() return &u } -func NewUserScreen(name string, screen io.WriteCloser) *UserScreen { - return &UserScreen{ - User: NewUser(name), - WriteCloser: screen, - } -} - -func (u *User) Config() UserConfig { - u.mu.Lock() - defer u.mu.Unlock() - return u.config -} - -func (u *User) SetConfig(cfg UserConfig) { - u.mu.Lock() - u.config = cfg - u.mu.Unlock() -} - -func (u *User) ID() string { - u.mu.Lock() - defer u.mu.Unlock() - return SanitizeName(u.name) -} - -func (u *User) Color() int { - return u.colorIdx -} - func (u *User) Name() string { - u.mu.Lock() - defer u.mu.Unlock() return u.name } -func (u *User) Joined() time.Time { - return u.joined +func (u *User) Color() int { + return u.config.Seed } -// Rename the user with a new Identifier. -func (u *User) SetName(name string) { - u.mu.Lock() - u.name = name - u.setColorIdx(rand.Int()) - u.mu.Unlock() -} - -// ReplyTo returns the last user that messaged this user. -func (u *User) ReplyTo() Author { - u.mu.Lock() - defer u.mu.Unlock() - return u.replyTo -} - -// SetReplyTo sets the last user to message this user. -func (u *User) SetReplyTo(user Author) { - // TODO: Use UserConfig.ReplyTo string - u.mu.Lock() - defer u.mu.Unlock() - u.replyTo = user -} - -// setColorIdx will set the colorIdx to a specific value, primarily used for -// testing. -func (u *User) setColorIdx(idx int) { - u.colorIdx = idx -} - -// Disconnect user, stop accepting messages -func (u *User) Close() { - u.closeOnce.Do(func() { - // close(u.msg) TODO: Close? - close(u.done) - }) -} - -// SetHighlight sets the highlighting regular expression to match string. -func (u *User) SetHighlight(s string) error { - re, err := regexp.Compile(fmt.Sprintf(reHighlight, s)) - if err != nil { - return err - } - u.mu.Lock() - u.config.Highlight = re - u.mu.Unlock() - return nil -} - -func (u *User) render(m Message) string { - cfg := u.Config() - switch m := m.(type) { - case PublicMsg: - return m.RenderFor(cfg) + Newline - case PrivateMsg: - u.SetReplyTo(m.From()) - return m.Render(cfg.Theme) + Newline - default: - return m.Render(cfg.Theme) + Newline - } -} - -// Add message to consume by user -func (u *User) Send(m Message) error { - select { - case <-u.done: - return ErrUserClosed - case u.msg <- m: - case <-time.After(messageTimeout): - logger.Printf("Message buffer full, closing: %s", u.Name()) - u.Close() - return ErrUserClosed - } - return nil -} - -// Prompt renders a theme-colorized prompt string. -func (u *User) Prompt() string { - name := u.Name() - cfg := u.Config() - if cfg.Theme != nil { - name = cfg.Theme.ColorName(u) - } - return fmt.Sprintf("[%s] ", name) -} - -// Container for per-user configurations. -type UserConfig struct { - Highlight *regexp.Regexp - Bell bool - Quiet bool - Theme *Theme -} - -// Default user configuration to use -var DefaultUserConfig UserConfig - -func init() { - DefaultUserConfig = UserConfig{ - Bell: true, - Quiet: false, - } - - // TODO: Seed random? +func (u *User) ID() string { + return SanitizeName(u.name) } diff --git a/chat/message/user_test.go b/chat/message/user_test.go index 437015e..6e7d8b0 100644 --- a/chat/message/user_test.go +++ b/chat/message/user_test.go @@ -9,7 +9,8 @@ func TestMakeUser(t *testing.T) { var actual, expected []byte s := &MockScreen{} - u := NewUserScreen("foo", s) + u := PipedScreen("foo", s) + m := NewAnnounceMsg("hello") defer u.Close() @@ -17,7 +18,6 @@ func TestMakeUser(t *testing.T) { if err != nil { t.Fatalf("failed to send: %s", err) } - u.HandleMsg(<-u.msg) s.Read(&actual) expected = []byte(m.String() + Newline) diff --git a/chat/room.go b/chat/room.go index 0e4e33f..9616807 100644 --- a/chat/room.go +++ b/chat/room.go @@ -88,7 +88,7 @@ func (r *Room) HandleMsg(m message.Message) { } r.history.Add(m) - r.Members.Each(func(_ string, item set.Item) (err error) { + r.Members.Each(func(k string, item set.Item) (err error) { roomMember := item.Value().(*roomMember) user := roomMember.Member from := fromMsg.From().(Member) diff --git a/chat/room_test.go b/chat/room_test.go index eb215c7..e1b9d08 100644 --- a/chat/room_test.go +++ b/chat/room_test.go @@ -57,7 +57,7 @@ func TestIgnore(t *testing.T) { users := make([]ScreenedUser, 3) for i := 0; i < 3; i++ { screen := &MockScreen{} - user := message.NewUserScreen(fmt.Sprintf("user%d", i), screen) + user := message.NewScreen(fmt.Sprintf("user%d", i), screen) users[i] = ScreenedUser{ user: user, screen: screen, @@ -162,7 +162,7 @@ func TestRoomJoin(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen("foo", s) + u := message.PipedScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -173,7 +173,6 @@ func TestRoomJoin(t *testing.T) { t.Fatal(err) } - u.HandleMsg(u.ConsumeOne()) expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -181,7 +180,6 @@ func TestRoomJoin(t *testing.T) { } ch.Send(message.NewSystemMsg("hello", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("-> hello" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -189,7 +187,6 @@ func TestRoomJoin(t *testing.T) { } ch.Send(message.ParseInput("/me says hello.", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("** foo says hello." + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -198,7 +195,11 @@ func TestRoomJoin(t *testing.T) { } func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { - u := message.NewUser("foo") + msgs := make(chan message.Message) + u := message.HandledScreen("foo", func(m message.Message) error { + msgs <- m + return nil + }) u.SetConfig(message.UserConfig{ Quiet: true, }) @@ -211,19 +212,12 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { t.Fatal(err) } - // Drain the initial Join message - <-ch.broadcast - go func() { - /* - for { - msg := u.ConsumeChan() - if _, ok := msg.(*message.AnnounceMsg); ok { - t.Errorf("Got unexpected `%T`", msg) - } + for msg := range msgs { + if _, ok := msg.(*message.AnnounceMsg); ok { + t.Errorf("Got unexpected `%T`", msg) } - */ - // XXX: Fix this + } }() // Call with an AnnounceMsg and all the other types @@ -234,10 +228,16 @@ func TestRoomDoesntBroadcastAnnounceMessagesWhenQuiet(t *testing.T) { ch.HandleMsg(message.NewSystemMsg("hello", u)) ch.HandleMsg(message.NewPrivateMsg("hello", u, u)) ch.HandleMsg(message.NewPublicMsg("hello", u)) + // Try an ignored one again just in case + ch.HandleMsg(message.NewAnnounceMsg("Once more for fun")) } func TestRoomQuietToggleBroadcasts(t *testing.T) { - u := message.NewUser("foo") + msgs := make(chan message.Message) + u := message.HandledScreen("foo", func(m message.Message) error { + msgs <- m + return nil + }) u.SetConfig(message.UserConfig{ Quiet: true, }) @@ -259,7 +259,7 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) { expectedMsg := message.NewAnnounceMsg("Ignored") ch.HandleMsg(expectedMsg) - msg := u.ConsumeOne() + msg := <-msgs if _, ok := msg.(*message.AnnounceMsg); !ok { t.Errorf("Got: `%T`; Expected: `%T`", msg, expectedMsg) } @@ -270,7 +270,7 @@ func TestRoomQuietToggleBroadcasts(t *testing.T) { ch.HandleMsg(message.NewAnnounceMsg("Ignored")) ch.HandleMsg(message.NewSystemMsg("hello", u)) - msg = u.ConsumeOne() + msg = <-msgs if _, ok := msg.(*message.AnnounceMsg); ok { t.Errorf("Got unexpected `%T`", msg) } @@ -280,7 +280,7 @@ func TestQuietToggleDisplayState(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen("foo", s) + u := message.PipedScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -291,7 +291,6 @@ func TestQuietToggleDisplayState(t *testing.T) { t.Fatal(err) } - u.HandleMsg(u.ConsumeOne()) expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -300,7 +299,6 @@ func TestQuietToggleDisplayState(t *testing.T) { ch.Send(message.ParseInput("/quiet", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("-> Quiet mode is toggled ON" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -309,7 +307,6 @@ func TestQuietToggleDisplayState(t *testing.T) { ch.Send(message.ParseInput("/quiet", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("-> Quiet mode is toggled OFF" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -321,7 +318,7 @@ func TestRoomNames(t *testing.T) { var expected, actual []byte s := &MockScreen{} - u := message.NewUserScreen("foo", s) + u := message.PipedScreen("foo", s) ch := NewRoom() go ch.Serve() @@ -332,7 +329,6 @@ func TestRoomNames(t *testing.T) { t.Fatal(err) } - u.HandleMsg(u.ConsumeOne()) expected = []byte(" * foo joined. (Connected: 1)" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { @@ -341,7 +337,6 @@ func TestRoomNames(t *testing.T) { ch.Send(message.ParseInput("/names", u)) - u.HandleMsg(u.ConsumeOne()) expected = []byte("-> 1 connected: foo" + message.Newline) s.Read(&actual) if !reflect.DeepEqual(actual, expected) { diff --git a/host.go b/host.go index dfc7d55..89d72ce 100644 --- a/host.go +++ b/host.go @@ -75,7 +75,7 @@ func (h *Host) SetMotd(motd string) { // Connect a specific Terminal to this host and its room. func (h *Host) Connect(term *sshd.Terminal) { requestedName := term.Conn.Name() - user := message.NewUserScreen(requestedName, term) + user := message.NewScreen(requestedName, term) client := h.addClient(user, term.Conn) defer h.removeClient(user, client)