/timestamp: Toggle timestamps after 30min of inactivity
[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) Config() UserConfig {
63         u.mu.Lock()
64         defer u.mu.Unlock()
65         return u.config
66 }
67
68 func (u *User) SetConfig(cfg UserConfig) {
69         u.mu.Lock()
70         u.config = cfg
71         u.mu.Unlock()
72 }
73
74 // Rename the user with a new Identifier.
75 func (u *User) SetID(id string) {
76         u.Identifier.SetID(id)
77         u.setColorIdx(rand.Int())
78 }
79
80 // ReplyTo returns the last user that messaged this user.
81 func (u *User) ReplyTo() *User {
82         u.mu.Lock()
83         defer u.mu.Unlock()
84         return u.replyTo
85 }
86
87 // SetReplyTo sets the last user to message this user.
88 func (u *User) SetReplyTo(user *User) {
89         u.mu.Lock()
90         defer u.mu.Unlock()
91         u.replyTo = user
92 }
93
94 // setColorIdx will set the colorIdx to a specific value, primarily used for
95 // testing.
96 func (u *User) setColorIdx(idx int) {
97         u.colorIdx = idx
98 }
99
100 // Disconnect user, stop accepting messages
101 func (u *User) Close() {
102         u.closeOnce.Do(func() {
103                 if u.screen != nil {
104                         u.screen.Close()
105                 }
106                 // close(u.msg) TODO: Close?
107                 close(u.done)
108         })
109 }
110
111 // Consume message buffer into the handler. Will block, should be called in a
112 // goroutine.
113 func (u *User) Consume() {
114         for {
115                 select {
116                 case <-u.done:
117                         return
118                 case m, ok := <-u.msg:
119                         if !ok {
120                                 return
121                         }
122                         u.HandleMsg(m)
123                 }
124         }
125 }
126
127 // Consume one message and stop, mostly for testing
128 func (u *User) ConsumeOne() Message {
129         return <-u.msg
130 }
131
132 // Check if there are pending messages, used for testing
133 func (u *User) HasMessages() bool {
134         select {
135         case msg := <-u.msg:
136                 u.msg <- msg
137                 return true
138         default:
139                 return false
140         }
141 }
142
143 // SetHighlight sets the highlighting regular expression to match string.
144 func (u *User) SetHighlight(s string) error {
145         re, err := regexp.Compile(fmt.Sprintf(reHighlight, s))
146         if err != nil {
147                 return err
148         }
149         u.mu.Lock()
150         u.config.Highlight = re
151         u.mu.Unlock()
152         return nil
153 }
154
155 func (u *User) render(m Message) string {
156         cfg := u.Config()
157         switch m := m.(type) {
158         case PublicMsg:
159                 return m.RenderFor(cfg) + Newline
160         case *PrivateMsg:
161                 if cfg.Bell {
162                         return m.Render(cfg.Theme) + Bel + Newline
163                 }
164                 return m.Render(cfg.Theme) + Newline
165         default:
166                 return m.Render(cfg.Theme) + Newline
167         }
168 }
169
170 // writeMsg renders the message and attempts to write it, will Close the user
171 // if it fails.
172 func (u *User) writeMsg(m Message) error {
173         r := u.render(m)
174         _, err := u.screen.Write([]byte(r))
175         if err != nil {
176                 logger.Printf("Write failed to %s, closing: %s", u.Name(), err)
177                 u.Close()
178         }
179         return err
180 }
181
182 // HandleMsg will render the message to the screen, blocking.
183 func (u *User) HandleMsg(m Message) error {
184         u.mu.Lock()
185         cfg := u.config
186         lastMsg := u.lastMsg
187         u.lastMsg = m.Timestamp()
188         injectTimestamp := !lastMsg.IsZero() && cfg.Timestamp && u.lastMsg.Sub(lastMsg) > timestampTimeout
189         u.mu.Unlock()
190
191         if injectTimestamp {
192                 // Inject a timestamp at most once every timestampTimeout between message intervals
193                 ts := NewSystemMsg(fmt.Sprintf("Timestamp: %s", m.Timestamp().UTC().Format(timestampLayout)), u)
194                 if err := u.writeMsg(ts); err != nil {
195                         return err
196                 }
197         }
198
199         return u.writeMsg(m)
200 }
201
202 // Add message to consume by user
203 func (u *User) Send(m Message) error {
204         select {
205         case <-u.done:
206                 return ErrUserClosed
207         case u.msg <- m:
208         case <-time.After(messageTimeout):
209                 logger.Printf("Message buffer full, closing: %s", u.Name())
210                 u.Close()
211                 return ErrUserClosed
212         }
213         return nil
214 }
215
216 // Container for per-user configurations.
217 type UserConfig struct {
218         Highlight *regexp.Regexp
219         Bell      bool
220         Quiet     bool
221         Timestamp bool
222         Theme     *Theme
223 }
224
225 // Default user configuration to use
226 var DefaultUserConfig UserConfig
227
228 func init() {
229         DefaultUserConfig = UserConfig{
230                 Bell:      true,
231                 Quiet:     false,
232                 Timestamp: false,
233         }
234
235         // TODO: Seed random?
236 }