"regexp"
"sync"
"time"
+
+ "github.com/shazow/ssh-chat/set"
)
const messageBuffer = 5
+const messageTimeout = 5 * time.Second
const reHighlight = `\b(%s)\b`
+const timestampTimeout = 30 * time.Minute
var ErrUserClosed = errors.New("user closed")
// User definition, implemented set Item interface and io.Writer
type User struct {
Identifier
- Config UserConfig
+ Ignored *set.Set
colorIdx int
joined time.Time
msg chan Message
done chan struct{}
- mu sync.RWMutex
- replyTo *User // Set when user gets a /msg, for replying.
screen io.WriteCloser
closeOnce sync.Once
+
+ mu sync.Mutex
+ config UserConfig
+ replyTo *User // Set when user gets a /msg, for replying.
+ lastMsg time.Time // When the last message was rendered
}
func NewUser(identity Identifier) *User {
u := User{
Identifier: identity,
- Config: *DefaultUserConfig,
+ config: DefaultUserConfig,
joined: time.Now(),
msg: make(chan Message, messageBuffer),
- done: make(chan struct{}, 1),
+ done: make(chan struct{}),
+ Ignored: set.New(),
}
- u.SetColorIdx(rand.Int())
+ u.setColorIdx(rand.Int())
return &u
}
return u
}
+func (u *User) Joined() time.Time {
+ return u.joined
+}
+
+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()
+}
+
// Rename the user with a new Identifier.
-func (u *User) SetId(id string) {
- u.Identifier.SetId(id)
- u.SetColorIdx(rand.Int())
+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 {
+ u.mu.Lock()
+ defer u.mu.Unlock()
return u.replyTo
}
// SetReplyTo sets the last user to message this user.
func (u *User) SetReplyTo(user *User) {
+ u.mu.Lock()
+ defer u.mu.Unlock()
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
+// setColorIdx will set the colorIdx to a specific value, primarily used for
// testing.
-func (u *User) SetColorIdx(idx int) {
+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.mu.Lock()
if u.screen != nil {
u.screen.Close()
}
- close(u.msg)
+ // close(u.msg) TODO: Close?
close(u.done)
- u.msg = nil
- u.mu.Unlock()
})
}
-// Consume message buffer into an io.Writer. Will block, should be called in a
+// Consume message buffer into the handler. Will block, should be called in a
// goroutine.
-// TODO: Not sure if this is a great API.
func (u *User) Consume() {
- for m := range u.msg {
- u.HandleMsg(m)
+ for {
+ select {
+ case <-u.done:
+ return
+ case m, ok := <-u.msg:
+ if !ok {
+ return
+ }
+ u.HandleMsg(m)
+ }
}
}
return <-u.msg
}
+// Check if there are pending messages, used for testing
+func (u *User) HasMessages() bool {
+ select {
+ case msg := <-u.msg:
+ u.msg <- msg
+ return true
+ default:
+ return false
+ }
+}
+
// 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
+ u.mu.Lock()
+ u.config.Highlight = re
+ u.mu.Unlock()
return nil
}
func (u *User) render(m Message) string {
+ cfg := u.Config()
+ var out string
switch m := m.(type) {
- case *PublicMsg:
- return m.RenderFor(u.Config) + Newline
+ case PublicMsg:
+ if u == m.From() {
+ if !cfg.Echo {
+ return ""
+ }
+ out += m.RenderSelf(cfg)
+ } else {
+ out += m.RenderFor(cfg)
+ }
case *PrivateMsg:
- u.SetReplyTo(m.From())
- return m.Render(u.Config.Theme) + Newline
+ out += m.Render(cfg.Theme)
+ if cfg.Bell {
+ out += Bel
+ }
+ case *CommandMsg:
+ out += m.RenderSelf(cfg)
default:
- return m.Render(u.Config.Theme) + Newline
+ out += m.Render(cfg.Theme)
}
+ if cfg.Timeformat != nil {
+ ts := m.Timestamp()
+ if cfg.Timezone != nil {
+ ts = ts.In(cfg.Timezone)
+ } else {
+ ts = ts.UTC()
+ }
+ return cfg.Theme.Timestamp(ts.Format(*cfg.Timeformat)) + " " + out + Newline
+ }
+ return out + Newline
}
-// HandleMsg will render the message to the screen, blocking.
-func (u *User) HandleMsg(m Message) error {
+// writeMsg renders the message and attempts to write it, will Close the user
+// if it fails.
+func (u *User) writeMsg(m Message) error {
r := u.render(m)
_, err := u.screen.Write([]byte(r))
if err != nil {
return err
}
+// HandleMsg will render the message to the screen, blocking.
+func (u *User) HandleMsg(m Message) error {
+ u.mu.Lock()
+ u.lastMsg = m.Timestamp()
+ u.mu.Unlock()
+ return u.writeMsg(m)
+}
+
// Add message to consume by user
func (u *User) Send(m Message) error {
- u.mu.RLock()
- defer u.mu.RUnlock()
select {
+ case <-u.done:
+ return ErrUserClosed
case u.msg <- m:
- default:
- logger.Printf("Msg buffer full, closing: %s", u.Name())
+ case <-time.After(messageTimeout):
+ logger.Printf("Message buffer full, closing: %s", u.Name())
u.Close()
return ErrUserClosed
}
// Container for per-user configurations.
type UserConfig struct {
- Highlight *regexp.Regexp
- Bell bool
- Quiet bool
- Theme *Theme
+ Highlight *regexp.Regexp
+ Bell bool
+ Quiet bool
+ Echo bool // Echo shows your own messages after sending, disabled for bots
+ Timeformat *string
+ Timezone *time.Location
+ Theme *Theme
}
// Default user configuration to use
-var DefaultUserConfig *UserConfig
+var DefaultUserConfig UserConfig
func init() {
- DefaultUserConfig = &UserConfig{
+ DefaultUserConfig = UserConfig{
Bell: true,
+ Echo: true,
Quiet: false,
}
// TODO: Seed random?
}
+
+// RecentActiveUsers is a slice of *Users that knows how to be sorted by the time of the last message.
+type RecentActiveUsers []*User
+
+func (a RecentActiveUsers) Len() int { return len(a) }
+func (a RecentActiveUsers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a RecentActiveUsers) Less(i, j int) bool {
+ a[i].mu.Lock()
+ defer a[i].mu.Unlock()
+ a[j].mu.Lock()
+ defer a[j].mu.Unlock()
+
+ if a[i].lastMsg.IsZero() {
+ return a[i].joined.Before(a[j].joined)
+ } else {
+ return a[i].lastMsg.Before(a[j].lastMsg)
+ }
+
+}