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