broken: Split UserScreen into PipedScreen, BufferedScreen, HandledScreen

This commit is contained in:
Andrey Petrov 2016-09-11 16:48:50 -04:00
parent 33a76bb7f4
commit 50022e9e44
8 changed files with 263 additions and 232 deletions

24
chat/message/config.go Normal file
View File

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

206
chat/message/screen.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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