refactor: User.Config -> User.Config() and User.SetConfig(UserConfig)
[ssh-chat] / chat / command.go
1 package chat
2
3 // FIXME: Would be sweet if we could piggyback on a cli parser or something.
4
5 import (
6         "errors"
7         "fmt"
8         "strings"
9
10         "github.com/shazow/ssh-chat/chat/message"
11         "github.com/shazow/ssh-chat/set"
12 )
13
14 // The error returned when an invalid command is issued.
15 var ErrInvalidCommand = errors.New("invalid command")
16
17 // The error returned when a command is given without an owner.
18 var ErrNoOwner = errors.New("command without owner")
19
20 // The error returned when a command is performed without the necessary number
21 // of arguments.
22 var ErrMissingArg = errors.New("missing argument")
23
24 // The error returned when a command is added without a prefix.
25 var ErrMissingPrefix = errors.New("command missing prefix")
26
27 // Command is a definition of a handler for a command.
28 type Command struct {
29         // The command's key, such as /foo
30         Prefix string
31         // Extra help regarding arguments
32         PrefixHelp string
33         // If omitted, command is hidden from /help
34         Help    string
35         Handler func(*Room, message.CommandMsg) error
36         // Command requires Op permissions
37         Op bool
38 }
39
40 // Commands is a registry of available commands.
41 type Commands map[string]*Command
42
43 // Add will register a command. If help string is empty, it will be hidden from
44 // Help().
45 func (c Commands) Add(cmd Command) error {
46         if cmd.Prefix == "" {
47                 return ErrMissingPrefix
48         }
49
50         c[cmd.Prefix] = &cmd
51         return nil
52 }
53
54 // Alias will add another command for the same handler, won't get added to help.
55 func (c Commands) Alias(command string, alias string) error {
56         cmd, ok := c[command]
57         if !ok {
58                 return ErrInvalidCommand
59         }
60         c[alias] = cmd
61         return nil
62 }
63
64 // Run executes a command message.
65 func (c Commands) Run(room *Room, msg message.CommandMsg) error {
66         if msg.From() == nil {
67                 return ErrNoOwner
68         }
69
70         cmd, ok := c[msg.Command()]
71         if !ok {
72                 return ErrInvalidCommand
73         }
74
75         return cmd.Handler(room, msg)
76 }
77
78 // Help will return collated help text as one string.
79 func (c Commands) Help(showOp bool) string {
80         // Filter by op
81         op := []*Command{}
82         normal := []*Command{}
83         for _, cmd := range c {
84                 if cmd.Op {
85                         op = append(op, cmd)
86                 } else {
87                         normal = append(normal, cmd)
88                 }
89         }
90         help := "Available commands:" + message.Newline + NewCommandsHelp(normal).String()
91         if showOp {
92                 help += message.Newline + "-> Operator commands:" + message.Newline + NewCommandsHelp(op).String()
93         }
94         return help
95 }
96
97 var defaultCommands *Commands
98
99 func init() {
100         defaultCommands = &Commands{}
101         InitCommands(defaultCommands)
102 }
103
104 // InitCommands injects default commands into a Commands registry.
105 func InitCommands(c *Commands) {
106         c.Add(Command{
107                 Prefix: "/help",
108                 Handler: func(room *Room, msg message.CommandMsg) error {
109                         op := room.IsOp(msg.From())
110                         room.Send(message.NewSystemMsg(room.commands.Help(op), msg.From()))
111                         return nil
112                 },
113         })
114
115         c.Add(Command{
116                 Prefix: "/me",
117                 Handler: func(room *Room, msg message.CommandMsg) error {
118                         me := strings.TrimLeft(msg.Body(), "/me")
119                         if me == "" {
120                                 me = "is at a loss for words."
121                         } else {
122                                 me = me[1:]
123                         }
124
125                         room.Send(message.NewEmoteMsg(me, msg.From()))
126                         return nil
127                 },
128         })
129
130         c.Add(Command{
131                 Prefix: "/exit",
132                 Help:   "Exit the chat.",
133                 Handler: func(room *Room, msg message.CommandMsg) error {
134                         msg.From().Close()
135                         return nil
136                 },
137         })
138         c.Alias("/exit", "/quit")
139
140         c.Add(Command{
141                 Prefix:     "/nick",
142                 PrefixHelp: "NAME",
143                 Help:       "Rename yourself.",
144                 Handler: func(room *Room, msg message.CommandMsg) error {
145                         args := msg.Args()
146                         if len(args) != 1 {
147                                 return ErrMissingArg
148                         }
149                         u := msg.From()
150
151                         member, ok := room.MemberByID(u.ID())
152                         if !ok {
153                                 return errors.New("failed to find member")
154                         }
155
156                         oldID := member.ID()
157                         member.SetID(SanitizeName(args[0]))
158                         err := room.Rename(oldID, member)
159                         if err != nil {
160                                 member.SetID(oldID)
161                                 return err
162                         }
163                         return nil
164                 },
165         })
166
167         c.Add(Command{
168                 Prefix: "/names",
169                 Help:   "List users who are connected.",
170                 Handler: func(room *Room, msg message.CommandMsg) error {
171                         // TODO: colorize
172                         names := room.NamesPrefix("")
173                         body := fmt.Sprintf("%d connected: %s", len(names), strings.Join(names, ", "))
174                         room.Send(message.NewSystemMsg(body, msg.From()))
175                         return nil
176                 },
177         })
178         c.Alias("/names", "/list")
179
180         c.Add(Command{
181                 Prefix:     "/theme",
182                 PrefixHelp: "[colors|...]",
183                 Help:       "Set your color theme. (More themes: solarized, mono, hacker)",
184                 Handler: func(room *Room, msg message.CommandMsg) error {
185                         user := msg.From()
186                         args := msg.Args()
187                         cfg := user.Config()
188                         if len(args) == 0 {
189                                 theme := "plain"
190                                 if cfg.Theme != nil {
191                                         theme = cfg.Theme.ID()
192                                 }
193                                 body := fmt.Sprintf("Current theme: %s", theme)
194                                 room.Send(message.NewSystemMsg(body, user))
195                                 return nil
196                         }
197
198                         id := args[0]
199                         for _, t := range message.Themes {
200                                 if t.ID() == id {
201                                         cfg.Theme = &t
202                                         user.SetConfig(cfg)
203                                         body := fmt.Sprintf("Set theme: %s", id)
204                                         room.Send(message.NewSystemMsg(body, user))
205                                         return nil
206                                 }
207                         }
208                         return errors.New("theme not found")
209                 },
210         })
211
212         c.Add(Command{
213                 Prefix: "/quiet",
214                 Help:   "Silence room announcements.",
215                 Handler: func(room *Room, msg message.CommandMsg) error {
216                         u := msg.From()
217                         cfg := u.Config()
218                         cfg.Quiet = !cfg.Quiet
219                         u.SetConfig(cfg)
220
221                         var body string
222                         if cfg.Quiet {
223                                 body = "Quiet mode is toggled ON"
224                         } else {
225                                 body = "Quiet mode is toggled OFF"
226                         }
227                         room.Send(message.NewSystemMsg(body, u))
228                         return nil
229                 },
230         })
231
232         c.Add(Command{
233                 Prefix:     "/slap",
234                 PrefixHelp: "NAME",
235                 Handler: func(room *Room, msg message.CommandMsg) error {
236                         var me string
237                         args := msg.Args()
238                         if len(args) == 0 {
239                                 me = "slaps themselves around a bit with a large trout."
240                         } else {
241                                 me = fmt.Sprintf("slaps %s around a bit with a large trout.", strings.Join(args, " "))
242                         }
243
244                         room.Send(message.NewEmoteMsg(me, msg.From()))
245                         return nil
246                 },
247         })
248
249         c.Add(Command{
250                 Prefix:     "/ignore",
251                 PrefixHelp: "[USER]",
252                 Help:       "Hide messages from USER, /unignore USER to stop hiding.",
253                 Handler: func(room *Room, msg message.CommandMsg) error {
254                         id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/ignore"))
255                         if id == "" {
256                                 // Print ignored names, if any.
257                                 var names []string
258                                 msg.From().Ignored.Each(func(_ string, item set.Item) error {
259                                         names = append(names, item.Key())
260                                         return nil
261                                 })
262
263                                 var systemMsg string
264                                 if len(names) == 0 {
265                                         systemMsg = "0 users ignored."
266                                 } else {
267                                         systemMsg = fmt.Sprintf("%d ignored: %s", len(names), strings.Join(names, ", "))
268                                 }
269
270                                 room.Send(message.NewSystemMsg(systemMsg, msg.From()))
271                                 return nil
272                         }
273
274                         if id == msg.From().ID() {
275                                 return errors.New("cannot ignore self")
276                         }
277                         target, ok := room.MemberByID(id)
278                         if !ok {
279                                 return fmt.Errorf("user not found: %s", id)
280                         }
281
282                         err := msg.From().Ignored.Add(set.Itemize(id, target))
283                         if err == set.ErrCollision {
284                                 return fmt.Errorf("user already ignored: %s", id)
285                         } else if err != nil {
286                                 return err
287                         }
288
289                         room.Send(message.NewSystemMsg(fmt.Sprintf("Ignoring: %s", target.Name()), msg.From()))
290                         return nil
291                 },
292         })
293
294         c.Add(Command{
295                 Prefix:     "/unignore",
296                 PrefixHelp: "USER",
297                 Handler: func(room *Room, msg message.CommandMsg) error {
298                         id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/unignore"))
299                         if id == "" {
300                                 return errors.New("must specify user")
301                         }
302
303                         if err := msg.From().Ignored.Remove(id); err != nil {
304                                 return err
305                         }
306
307                         room.Send(message.NewSystemMsg(fmt.Sprintf("No longer ignoring: %s", id), msg.From()))
308                         return nil
309                 },
310         })
311 }