ssh-chat/chat/user.go
2015-01-18 18:55:47 -08:00

179 lines
3.5 KiB
Go

package chat
import (
"errors"
"fmt"
"io"
"math/rand"
"regexp"
"sync"
"time"
)
const messageBuffer = 20
const reHighlight = `\b(%s)\b`
var ErrUserClosed = errors.New("user closed")
// Identifier is an interface that can uniquely identify itself.
type Identifier interface {
Id() string
SetId(string)
Name() string
}
// User definition, implemented set Item interface and io.Writer
type User struct {
Identifier
Config UserConfig
colorIdx int
joined time.Time
msg chan Message
done chan struct{}
replyTo *User // Set when user gets a /msg, for replying.
closed bool
closeOnce sync.Once
}
func NewUser(identity Identifier) *User {
u := User{
Identifier: identity,
Config: *DefaultUserConfig,
joined: time.Now(),
msg: make(chan Message, messageBuffer),
done: make(chan struct{}, 1),
}
u.SetColorIdx(rand.Int())
return &u
}
func NewUserScreen(identity Identifier, screen io.Writer) *User {
u := NewUser(identity)
go u.Consume(screen)
return u
}
// Rename the user with a new Identifier.
func (u *User) SetId(id string) {
u.Identifier.SetId(id)
u.SetColorIdx(rand.Int())
}
// ReplyTo returns the last user that messaged this user.
func (u *User) ReplyTo() *User {
return u.replyTo
}
// SetReplyTo sets the last user to message this user.
func (u *User) SetReplyTo(user *User) {
u.replyTo = user
}
// ToggleQuietMode will toggle whether or not quiet mode is enabled
func (u *User) ToggleQuietMode() {
u.Config.Quiet = !u.Config.Quiet
}
// SetColorIdx will set the colorIdx to a specific value, primarily used for
// testing.
func (u *User) SetColorIdx(idx int) {
u.colorIdx = idx
}
// Block until user is closed
func (u *User) Wait() {
<-u.done
}
// Disconnect user, stop accepting messages
func (u *User) Close() {
u.closeOnce.Do(func() {
u.closed = true
close(u.done)
close(u.msg)
})
}
// Consume message buffer into an io.Writer. Will block, should be called in a
// goroutine.
// TODO: Not sure if this is a great API.
func (u *User) Consume(out io.Writer) {
for m := range u.msg {
u.HandleMsg(m, out)
}
}
// Consume one message and stop, mostly for testing
func (u *User) ConsumeOne(out io.Writer) {
u.HandleMsg(<-u.msg, out)
}
// 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.Config.Highlight = re
return nil
}
func (u *User) render(m Message) string {
switch m := m.(type) {
case *PublicMsg:
return m.RenderFor(u.Config) + Newline
case *PrivateMsg:
u.SetReplyTo(m.From())
return m.Render(u.Config.Theme) + Newline
default:
return m.Render(u.Config.Theme) + Newline
}
}
func (u *User) HandleMsg(m Message, out io.Writer) {
r := u.render(m)
_, err := out.Write([]byte(r))
if err != nil {
logger.Printf("Write failed to %s, closing: %s", u.Name(), err)
u.Close()
}
}
// Add message to consume by user
func (u *User) Send(m Message) error {
if u.closed {
return ErrUserClosed
}
select {
case u.msg <- m:
default:
logger.Printf("Msg buffer full, closing: %s", u.Name())
u.Close()
return ErrUserClosed
}
return nil
}
// 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?
}