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