refactor: User.Config -> User.Config() and User.SetConfig(UserConfig)
[ssh-chat] / chat / room.go
1 package chat
2
3 import (
4         "errors"
5         "fmt"
6         "io"
7         "sync"
8
9         "github.com/shazow/ssh-chat/chat/message"
10         "github.com/shazow/ssh-chat/set"
11 )
12
13 const historyLen = 20
14 const roomBuffer = 10
15
16 // The error returned when a message is sent to a room that is already
17 // closed.
18 var ErrRoomClosed = errors.New("room closed")
19
20 // The error returned when a user attempts to join with an invalid name, such
21 // as empty string.
22 var ErrInvalidName = errors.New("invalid name")
23
24 // Member is a User with per-Room metadata attached to it.
25 type Member struct {
26         *message.User
27 }
28
29 // Room definition, also a Set of User Items
30 type Room struct {
31         topic     string
32         history   *message.History
33         broadcast chan message.Message
34         commands  Commands
35         closed    bool
36         closeOnce sync.Once
37
38         Members *set.Set
39         Ops     *set.Set
40 }
41
42 // NewRoom creates a new room.
43 func NewRoom() *Room {
44         broadcast := make(chan message.Message, roomBuffer)
45
46         return &Room{
47                 broadcast: broadcast,
48                 history:   message.NewHistory(historyLen),
49                 commands:  *defaultCommands,
50
51                 Members: set.New(),
52                 Ops:     set.New(),
53         }
54 }
55
56 // SetCommands sets the room's command handlers.
57 func (r *Room) SetCommands(commands Commands) {
58         r.commands = commands
59 }
60
61 // Close the room and all the users it contains.
62 func (r *Room) Close() {
63         r.closeOnce.Do(func() {
64                 r.closed = true
65                 r.Members.Each(func(_ string, item set.Item) error {
66                         item.Value().(*Member).Close()
67                         return nil
68                 })
69                 r.Members.Clear()
70                 close(r.broadcast)
71         })
72 }
73
74 // SetLogging sets logging output for the room's history
75 func (r *Room) SetLogging(out io.Writer) {
76         r.history.SetOutput(out)
77 }
78
79 // HandleMsg reacts to a message, will block until done.
80 func (r *Room) HandleMsg(m message.Message) {
81         switch m := m.(type) {
82         case *message.CommandMsg:
83                 cmd := *m
84                 err := r.commands.Run(r, cmd)
85                 if err != nil {
86                         m := message.NewSystemMsg(fmt.Sprintf("Err: %s", err), cmd.From())
87                         go r.HandleMsg(m)
88                 }
89         case message.MessageTo:
90                 user := m.To()
91                 user.Send(m)
92         default:
93                 fromMsg, skip := m.(message.MessageFrom)
94                 var skipUser *message.User
95                 if skip {
96                         skipUser = fromMsg.From()
97                 }
98
99                 r.history.Add(m)
100                 r.Members.Each(func(_ string, item set.Item) (err error) {
101                         user := item.Value().(*Member).User
102
103                         if fromMsg != nil && user.Ignored.In(fromMsg.From().ID()) {
104                                 // Skip because ignored
105                                 return
106                         }
107
108                         if skip && skipUser == user {
109                                 // Skip self
110                                 return
111                         }
112                         if _, ok := m.(*message.AnnounceMsg); ok {
113                                 if user.Config().Quiet {
114                                         // Skip announcements
115                                         return
116                                 }
117                         }
118                         user.Send(m)
119                         return
120                 })
121         }
122 }
123
124 // Serve will consume the broadcast room and handle the messages, should be
125 // run in a goroutine.
126 func (r *Room) Serve() {
127         for m := range r.broadcast {
128                 go r.HandleMsg(m)
129         }
130 }
131
132 // Send message, buffered by a chan.
133 func (r *Room) Send(m message.Message) {
134         r.broadcast <- m
135 }
136
137 // History feeds the room's recent message history to the user's handler.
138 func (r *Room) History(u *message.User) {
139         for _, m := range r.history.Get(historyLen) {
140                 u.Send(m)
141         }
142 }
143
144 // Join the room as a user, will announce.
145 func (r *Room) Join(u *message.User) (*Member, error) {
146         // TODO: Check if closed
147         if u.ID() == "" {
148                 return nil, ErrInvalidName
149         }
150         member := &Member{u}
151         err := r.Members.Add(set.Itemize(u.ID(), member))
152         if err != nil {
153                 return nil, err
154         }
155         r.History(u)
156         s := fmt.Sprintf("%s joined. (Connected: %d)", u.Name(), r.Members.Len())
157         r.Send(message.NewAnnounceMsg(s))
158         return member, nil
159 }
160
161 // Leave the room as a user, will announce. Mostly used during setup.
162 func (r *Room) Leave(u message.Identifier) error {
163         err := r.Members.Remove(u.ID())
164         if err != nil {
165                 return err
166         }
167         r.Ops.Remove(u.ID())
168         s := fmt.Sprintf("%s left.", u.Name())
169         r.Send(message.NewAnnounceMsg(s))
170         return nil
171 }
172
173 // Rename member with a new identity. This will not call rename on the member.
174 func (r *Room) Rename(oldID string, u message.Identifier) error {
175         if u.ID() == "" {
176                 return ErrInvalidName
177         }
178         err := r.Members.Replace(oldID, set.Itemize(u.ID(), u))
179         if err != nil {
180                 return err
181         }
182
183         s := fmt.Sprintf("%s is now known as %s.", oldID, u.ID())
184         r.Send(message.NewAnnounceMsg(s))
185         return nil
186 }
187
188 // Member returns a corresponding Member object to a User if the Member is
189 // present in this room.
190 func (r *Room) Member(u *message.User) (*Member, bool) {
191         m, ok := r.MemberByID(u.ID())
192         if !ok {
193                 return nil, false
194         }
195         // Check that it's the same user
196         if m.User != u {
197                 return nil, false
198         }
199         return m, true
200 }
201
202 func (r *Room) MemberByID(id string) (*Member, bool) {
203         m, err := r.Members.Get(id)
204         if err != nil {
205                 return nil, false
206         }
207         return m.Value().(*Member), true
208 }
209
210 // IsOp returns whether a user is an operator in this room.
211 func (r *Room) IsOp(u *message.User) bool {
212         return r.Ops.In(u.ID())
213 }
214
215 // Topic of the room.
216 func (r *Room) Topic() string {
217         return r.topic
218 }
219
220 // SetTopic will set the topic of the room.
221 func (r *Room) SetTopic(s string) {
222         r.topic = s
223 }
224
225 // NamesPrefix lists all members' names with a given prefix, used to query
226 // for autocompletion purposes.
227 func (r *Room) NamesPrefix(prefix string) []string {
228         items := r.Members.ListPrefix(prefix)
229         names := make([]string, len(items))
230         for i, item := range items {
231                 names[i] = item.Value().(*Member).User.Name()
232         }
233         return names
234 }