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: Screen(name), WriteCloser: screen, } } func HandledScreen(name string, handler func(Message) error) *handledScreen { return &handledScreen{ baseScreen: Screen(name), handler: handler, } } func Screen(name string) *baseScreen { return &baseScreen{ user: NewUser(name), } } 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) } } }