"sync"
"time"
- "github.com/shazow/ssh-chat/common"
+ "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
+ Ignored *set.Set
colorIdx int
joined time.Time
msg chan Message
done chan struct{}
- Ignored *common.IdSet
screen io.WriteCloser
closeOnce sync.Once
mu sync.Mutex
- Config UserConfig
- replyTo *User // Set when user gets a /msg, for replying.
+ 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{}),
- Ignored: common.NewIdSet(),
+ 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.
u.replyTo = user
}
-// ToggleQuietMode will toggle whether or not quiet mode is enabled
-func (u *User) ToggleQuietMode() {
- u.mu.Lock()
- defer u.mu.Unlock()
- 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() {
return err
}
u.mu.Lock()
- u.Config.Highlight = re
+ u.config.Highlight = re
u.mu.Unlock()
return nil
}
func (u *User) render(m Message) string {
- u.mu.Lock()
- cfg := u.Config
- u.mu.Unlock()
+ cfg := u.Config()
+ var out string
switch m := m.(type) {
case PublicMsg:
- return m.RenderFor(cfg) + Newline
- case PrivateMsg:
- u.SetReplyTo(m.From())
- return m.Render(cfg.Theme) + Newline
+ if u == m.From() {
+ if !cfg.Echo {
+ return ""
+ }
+ out += m.RenderSelf(cfg)
+ } else {
+ out += m.RenderFor(cfg)
+ }
+ case *PrivateMsg:
+ out += m.Render(cfg.Theme)
+ if cfg.Bell {
+ out += Bel
+ }
+ case *CommandMsg:
+ out += m.RenderSelf(cfg)
default:
- return m.Render(cfg.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 {
select {
- case u.msg <- m:
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 nil
}
-func (u *User) Ignore(identified common.Identified) error {
- if identified == nil {
- return errors.New("user is nil.")
- }
-
- if identified.Id() == u.Id() {
- return errors.New("cannot ignore self.")
- }
-
- if u.Ignored.In(identified) {
- return errors.New("user already ignored.")
- }
-
- u.Ignored.Add(identified)
- return nil
-}
-
-func (u *User) Unignore(id string) error {
- if id == "" {
- return errors.New("user is nil.")
- }
-
- identified, err := u.Ignored.Get(id)
- if err != nil {
- return err
- }
-
- return u.Ignored.Remove(identified)
-}
-
// 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
func init() {
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)
+ }
+
+}