refactor: User.Config -> User.Config() and User.SetConfig(UserConfig)
[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 func (u *User) Config() UserConfig {
60         u.mu.Lock()
61         defer u.mu.Unlock()
62         return u.config
63 }
64
65 func (u *User) SetConfig(cfg UserConfig) {
66         u.mu.Lock()
67         u.config = cfg
68         u.mu.Unlock()
69 }
70
71 // Rename the user with a new Identifier.
72 func (u *User) SetID(id string) {
73         u.Identifier.SetID(id)
74         u.setColorIdx(rand.Int())
75 }
76
77 // ReplyTo returns the last user that messaged this user.
78 func (u *User) ReplyTo() *User {
79         u.mu.Lock()
80         defer u.mu.Unlock()
81         return u.replyTo
82 }
83
84 // SetReplyTo sets the last user to message this user.
85 func (u *User) SetReplyTo(user *User) {
86         u.mu.Lock()
87         defer u.mu.Unlock()
88         u.replyTo = user
89 }
90
91 // setColorIdx will set the colorIdx to a specific value, primarily used for
92 // testing.
93 func (u *User) setColorIdx(idx int) {
94         u.colorIdx = idx
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         cfg := u.Config()
154         switch m := m.(type) {
155         case PublicMsg:
156                 return m.RenderFor(cfg) + Newline
157         case PrivateMsg:
158                 u.SetReplyTo(m.From())
159                 return m.Render(cfg.Theme) + Newline
160         default:
161                 return m.Render(cfg.Theme) + Newline
162         }
163 }
164
165 // HandleMsg will render the message to the screen, blocking.
166 func (u *User) HandleMsg(m Message) error {
167         r := u.render(m)
168         _, err := u.screen.Write([]byte(r))
169         if err != nil {
170                 logger.Printf("Write failed to %s, closing: %s", u.Name(), err)
171                 u.Close()
172         }
173         return err
174 }
175
176 // Add message to consume by user
177 func (u *User) Send(m Message) error {
178         select {
179         case <-u.done:
180                 return ErrUserClosed
181         case u.msg <- m:
182         case <-time.After(messageTimeout):
183                 logger.Printf("Message buffer full, closing: %s", u.Name())
184                 u.Close()
185                 return ErrUserClosed
186         }
187         return nil
188 }
189
190 // Container for per-user configurations.
191 type UserConfig struct {
192         Highlight *regexp.Regexp
193         Bell      bool
194         Quiet     bool
195         Theme     *Theme
196 }
197
198 // Default user configuration to use
199 var DefaultUserConfig UserConfig
200
201 func init() {
202         DefaultUserConfig = UserConfig{
203                 Bell:  true,
204                 Quiet: false,
205         }
206
207         // TODO: Seed random?
208 }