chat/message: Fix RecentActiveUsers sort order
[ssh-chat] / chat / message / user.go
1 package message
2
3 import (
4         "errors"
5         "fmt"
6         "io"
7         "math/rand"
8         "regexp"
9         "sync"
10         "time"
11
12         "github.com/shazow/ssh-chat/set"
13 )
14
15 const messageBuffer = 5
16 const messageTimeout = 5 * time.Second
17 const reHighlight = `\b(%s)\b`
18 const timestampTimeout = 30 * time.Minute
19
20 var ErrUserClosed = errors.New("user closed")
21
22 // User definition, implemented set Item interface and io.Writer
23 type User struct {
24         Identifier
25         Ignored  *set.Set
26         colorIdx int
27         joined   time.Time
28         msg      chan Message
29         done     chan struct{}
30
31         screen    io.WriteCloser
32         closeOnce sync.Once
33
34         mu      sync.Mutex
35         config  UserConfig
36         replyTo *User     // Set when user gets a /msg, for replying.
37         lastMsg time.Time // When the last message was rendered
38 }
39
40 func NewUser(identity Identifier) *User {
41         u := User{
42                 Identifier: identity,
43                 config:     DefaultUserConfig,
44                 joined:     time.Now(),
45                 msg:        make(chan Message, messageBuffer),
46                 done:       make(chan struct{}),
47                 Ignored:    set.New(),
48         }
49         u.setColorIdx(rand.Int())
50
51         return &u
52 }
53
54 func NewUserScreen(identity Identifier, screen io.WriteCloser) *User {
55         u := NewUser(identity)
56         u.screen = screen
57
58         return u
59 }
60
61 func (u *User) Joined() time.Time {
62         return u.joined
63 }
64
65 func (u *User) Config() UserConfig {
66         u.mu.Lock()
67         defer u.mu.Unlock()
68         return u.config
69 }
70
71 func (u *User) SetConfig(cfg UserConfig) {
72         u.mu.Lock()
73         u.config = cfg
74         u.mu.Unlock()
75 }
76
77 // Rename the user with a new Identifier.
78 func (u *User) SetID(id string) {
79         u.Identifier.SetID(id)
80         u.setColorIdx(rand.Int())
81 }
82
83 // ReplyTo returns the last user that messaged this user.
84 func (u *User) ReplyTo() *User {
85         u.mu.Lock()
86         defer u.mu.Unlock()
87         return u.replyTo
88 }
89
90 // SetReplyTo sets the last user to message this user.
91 func (u *User) SetReplyTo(user *User) {
92         u.mu.Lock()
93         defer u.mu.Unlock()
94         u.replyTo = user
95 }
96
97 // setColorIdx will set the colorIdx to a specific value, primarily used for
98 // testing.
99 func (u *User) setColorIdx(idx int) {
100         u.colorIdx = idx
101 }
102
103 // Disconnect user, stop accepting messages
104 func (u *User) Close() {
105         u.closeOnce.Do(func() {
106                 if u.screen != nil {
107                         u.screen.Close()
108                 }
109                 // close(u.msg) TODO: Close?
110                 close(u.done)
111         })
112 }
113
114 // Consume message buffer into the handler. Will block, should be called in a
115 // goroutine.
116 func (u *User) Consume() {
117         for {
118                 select {
119                 case <-u.done:
120                         return
121                 case m, ok := <-u.msg:
122                         if !ok {
123                                 return
124                         }
125                         u.HandleMsg(m)
126                 }
127         }
128 }
129
130 // Consume one message and stop, mostly for testing
131 func (u *User) ConsumeOne() Message {
132         return <-u.msg
133 }
134
135 // Check if there are pending messages, used for testing
136 func (u *User) HasMessages() bool {
137         select {
138         case msg := <-u.msg:
139                 u.msg <- msg
140                 return true
141         default:
142                 return false
143         }
144 }
145
146 // SetHighlight sets the highlighting regular expression to match string.
147 func (u *User) SetHighlight(s string) error {
148         re, err := regexp.Compile(fmt.Sprintf(reHighlight, s))
149         if err != nil {
150                 return err
151         }
152         u.mu.Lock()
153         u.config.Highlight = re
154         u.mu.Unlock()
155         return nil
156 }
157
158 func (u *User) render(m Message) string {
159         cfg := u.Config()
160         var out string
161         switch m := m.(type) {
162         case PublicMsg:
163                 if u == m.From() {
164                         if !cfg.Echo {
165                                 return ""
166                         }
167                         out += m.RenderSelf(cfg)
168                 } else {
169                         out += m.RenderFor(cfg)
170                 }
171         case *PrivateMsg:
172                 out += m.Render(cfg.Theme)
173                 if cfg.Bell {
174                         out += Bel
175                 }
176         case *CommandMsg:
177                 out += m.RenderSelf(cfg)
178         default:
179                 out += m.Render(cfg.Theme)
180         }
181         if cfg.Timeformat != nil {
182                 ts := m.Timestamp()
183                 if cfg.Timezone != nil {
184                         ts = ts.In(cfg.Timezone)
185                 } else {
186                         ts = ts.UTC()
187                 }
188                 return cfg.Theme.Timestamp(ts.Format(*cfg.Timeformat)) + "  " + out + Newline
189         }
190         return out + Newline
191 }
192
193 // writeMsg renders the message and attempts to write it, will Close the user
194 // if it fails.
195 func (u *User) writeMsg(m Message) error {
196         r := u.render(m)
197         _, err := u.screen.Write([]byte(r))
198         if err != nil {
199                 logger.Printf("Write failed to %s, closing: %s", u.Name(), err)
200                 u.Close()
201         }
202         return err
203 }
204
205 // HandleMsg will render the message to the screen, blocking.
206 func (u *User) HandleMsg(m Message) error {
207         u.mu.Lock()
208         u.lastMsg = m.Timestamp()
209         u.mu.Unlock()
210         return u.writeMsg(m)
211 }
212
213 // Add message to consume by user
214 func (u *User) Send(m Message) error {
215         select {
216         case <-u.done:
217                 return ErrUserClosed
218         case u.msg <- m:
219         case <-time.After(messageTimeout):
220                 logger.Printf("Message buffer full, closing: %s", u.Name())
221                 u.Close()
222                 return ErrUserClosed
223         }
224         return nil
225 }
226
227 // Container for per-user configurations.
228 type UserConfig struct {
229         Highlight  *regexp.Regexp
230         Bell       bool
231         Quiet      bool
232         Echo       bool // Echo shows your own messages after sending, disabled for bots
233         Timeformat *string
234         Timezone   *time.Location
235         Theme      *Theme
236 }
237
238 // Default user configuration to use
239 var DefaultUserConfig UserConfig
240
241 func init() {
242         DefaultUserConfig = UserConfig{
243                 Bell:  true,
244                 Echo:  true,
245                 Quiet: false,
246         }
247
248         // TODO: Seed random?
249 }
250
251 // RecentActiveUsers is a slice of *Users that knows how to be sorted by the time of the last message.
252 type RecentActiveUsers []*User
253
254 func (a RecentActiveUsers) Len() int      { return len(a) }
255 func (a RecentActiveUsers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
256 func (a RecentActiveUsers) Less(i, j int) bool {
257         a[i].mu.Lock()
258         defer a[i].mu.Unlock()
259         a[j].mu.Lock()
260         defer a[j].mu.Unlock()
261
262         if a[i].lastMsg.IsZero() {
263                 return a[i].joined.Before(a[j].joined)
264         } else {
265                 return a[i].lastMsg.Before(a[j].lastMsg)
266         }
267
268 }