refactor: User.SetColorIdx -> User.setColorIdx, preparing to abstract user
[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
19 var ErrUserClosed = errors.New("user closed")
20
21 // User definition, implemented set Item interface and io.Writer
22 type User struct {
23         Identifier
24         Ignored  *set.Set
25         colorIdx int
26         joined   time.Time
27         msg      chan Message
28         done     chan struct{}
29
30         screen    io.WriteCloser
31         closeOnce sync.Once
32
33         mu      sync.Mutex
34         Config  UserConfig
35         replyTo *User // Set when user gets a /msg, for replying.
36 }
37
38 func NewUser(identity Identifier) *User {
39         u := User{
40                 Identifier: identity,
41                 Config:     DefaultUserConfig,
42                 joined:     time.Now(),
43                 msg:        make(chan Message, messageBuffer),
44                 done:       make(chan struct{}),
45                 Ignored:    set.New(),
46         }
47         u.setColorIdx(rand.Int())
48
49         return &u
50 }
51
52 func NewUserScreen(identity Identifier, screen io.WriteCloser) *User {
53         u := NewUser(identity)
54         u.screen = screen
55
56         return u
57 }
58
59 // Rename the user with a new Identifier.
60 func (u *User) SetID(id string) {
61         u.Identifier.SetID(id)
62         u.setColorIdx(rand.Int())
63 }
64
65 // ReplyTo returns the last user that messaged this user.
66 func (u *User) ReplyTo() *User {
67         u.mu.Lock()
68         defer u.mu.Unlock()
69         return u.replyTo
70 }
71
72 // SetReplyTo sets the last user to message this user.
73 func (u *User) SetReplyTo(user *User) {
74         u.mu.Lock()
75         defer u.mu.Unlock()
76         u.replyTo = user
77 }
78
79 // ToggleQuietMode will toggle whether or not quiet mode is enabled
80 func (u *User) ToggleQuietMode() {
81         u.mu.Lock()
82         defer u.mu.Unlock()
83         u.Config.Quiet = !u.Config.Quiet
84 }
85
86 // setColorIdx will set the colorIdx to a specific value, primarily used for
87 // testing.
88 func (u *User) setColorIdx(idx int) {
89         u.colorIdx = idx
90 }
91
92 // Block until user is closed
93 func (u *User) Wait() {
94         <-u.done
95 }
96
97 // Disconnect user, stop accepting messages
98 func (u *User) Close() {
99         u.closeOnce.Do(func() {
100                 if u.screen != nil {
101                         u.screen.Close()
102                 }
103                 // close(u.msg) TODO: Close?
104                 close(u.done)
105         })
106 }
107
108 // Consume message buffer into the handler. Will block, should be called in a
109 // goroutine.
110 func (u *User) Consume() {
111         for {
112                 select {
113                 case <-u.done:
114                         return
115                 case m, ok := <-u.msg:
116                         if !ok {
117                                 return
118                         }
119                         u.HandleMsg(m)
120                 }
121         }
122 }
123
124 // Consume one message and stop, mostly for testing
125 func (u *User) ConsumeOne() Message {
126         return <-u.msg
127 }
128
129 // Check if there are pending messages, used for testing
130 func (u *User) HasMessages() bool {
131         select {
132         case msg := <-u.msg:
133                 u.msg <- msg
134                 return true
135         default:
136                 return false
137         }
138 }
139
140 // SetHighlight sets the highlighting regular expression to match string.
141 func (u *User) SetHighlight(s string) error {
142         re, err := regexp.Compile(fmt.Sprintf(reHighlight, s))
143         if err != nil {
144                 return err
145         }
146         u.mu.Lock()
147         u.Config.Highlight = re
148         u.mu.Unlock()
149         return nil
150 }
151
152 func (u *User) render(m Message) string {
153         u.mu.Lock()
154         cfg := u.Config
155         u.mu.Unlock()
156         switch m := m.(type) {
157         case PublicMsg:
158                 return m.RenderFor(cfg) + Newline
159         case PrivateMsg:
160                 u.SetReplyTo(m.From())
161                 return m.Render(cfg.Theme) + Newline
162         default:
163                 return m.Render(cfg.Theme) + Newline
164         }
165 }
166
167 // HandleMsg will render the message to the screen, blocking.
168 func (u *User) HandleMsg(m Message) error {
169         r := u.render(m)
170         _, err := u.screen.Write([]byte(r))
171         if err != nil {
172                 logger.Printf("Write failed to %s, closing: %s", u.Name(), err)
173                 u.Close()
174         }
175         return err
176 }
177
178 // Add message to consume by user
179 func (u *User) Send(m Message) error {
180         select {
181         case <-u.done:
182                 return ErrUserClosed
183         case u.msg <- m:
184         case <-time.After(messageTimeout):
185                 logger.Printf("Message buffer full, closing: %s", u.Name())
186                 u.Close()
187                 return ErrUserClosed
188         }
189         return nil
190 }
191
192 // Container for per-user configurations.
193 type UserConfig struct {
194         Highlight *regexp.Regexp
195         Bell      bool
196         Quiet     bool
197         Theme     *Theme
198 }
199
200 // Default user configuration to use
201 var DefaultUserConfig UserConfig
202
203 func init() {
204         DefaultUserConfig = UserConfig{
205                 Bell:  true,
206                 Quiet: false,
207         }
208
209         // TODO: Seed random?
210 }