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