a36e9d11bb84b58fdd672c761f417dac38b008f1
[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                         if len(args) == 0 {
188                                 theme := "plain"
189                                 if user.Config.Theme != nil {
190                                         theme = user.Config.Theme.ID()
191                                 }
192                                 body := fmt.Sprintf("Current theme: %s", theme)
193                                 room.Send(message.NewSystemMsg(body, user))
194                                 return nil
195                         }
196
197                         id := args[0]
198                         for _, t := range message.Themes {
199                                 if t.ID() == id {
200                                         user.Config.Theme = &t
201                                         body := fmt.Sprintf("Set theme: %s", id)
202                                         room.Send(message.NewSystemMsg(body, user))
203                                         return nil
204                                 }
205                         }
206                         return errors.New("theme not found")
207                 },
208         })
209
210         c.Add(Command{
211                 Prefix: "/quiet",
212                 Help:   "Silence room announcements.",
213                 Handler: func(room *Room, msg message.CommandMsg) error {
214                         u := msg.From()
215                         u.ToggleQuietMode()
216
217                         var body string
218                         if u.Config.Quiet {
219                                 body = "Quiet mode is toggled ON"
220                         } else {
221                                 body = "Quiet mode is toggled OFF"
222                         }
223                         room.Send(message.NewSystemMsg(body, u))
224                         return nil
225                 },
226         })
227
228         c.Add(Command{
229                 Prefix:     "/slap",
230                 PrefixHelp: "NAME",
231                 Handler: func(room *Room, msg message.CommandMsg) error {
232                         var me string
233                         args := msg.Args()
234                         if len(args) == 0 {
235                                 me = "slaps themselves around a bit with a large trout."
236                         } else {
237                                 me = fmt.Sprintf("slaps %s around a bit with a large trout.", strings.Join(args, " "))
238                         }
239
240                         room.Send(message.NewEmoteMsg(me, msg.From()))
241                         return nil
242                 },
243         })
244
245         c.Add(Command{
246                 Prefix:     "/ignore",
247                 PrefixHelp: "[USER]",
248                 Help:       "Hide messages from USER, /unignore USER to stop hiding.",
249                 Handler: func(room *Room, msg message.CommandMsg) error {
250                         id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/ignore"))
251                         if id == "" {
252                                 // Print ignored names, if any.
253                                 var names []string
254                                 msg.From().Ignored.Each(func(_ string, item set.Item) error {
255                                         names = append(names, item.Key())
256                                         return nil
257                                 })
258
259                                 var systemMsg string
260                                 if len(names) == 0 {
261                                         systemMsg = "0 users ignored."
262                                 } else {
263                                         systemMsg = fmt.Sprintf("%d ignored: %s", len(names), strings.Join(names, ", "))
264                                 }
265
266                                 room.Send(message.NewSystemMsg(systemMsg, msg.From()))
267                                 return nil
268                         }
269
270                         if id == msg.From().ID() {
271                                 return errors.New("cannot ignore self")
272                         }
273                         target, ok := room.MemberByID(id)
274                         if !ok {
275                                 return fmt.Errorf("user not found: %s", id)
276                         }
277
278                         err := msg.From().Ignored.Add(set.Itemize(id, target))
279                         if err == set.ErrCollision {
280                                 return fmt.Errorf("user already ignored: %s", id)
281                         } else if err != nil {
282                                 return err
283                         }
284
285                         room.Send(message.NewSystemMsg(fmt.Sprintf("Ignoring: %s", target.Name()), msg.From()))
286                         return nil
287                 },
288         })
289
290         c.Add(Command{
291                 Prefix:     "/unignore",
292                 PrefixHelp: "USER",
293                 Handler: func(room *Room, msg message.CommandMsg) error {
294                         id := strings.TrimSpace(strings.TrimLeft(msg.Body(), "/unignore"))
295                         if id == "" {
296                                 return errors.New("must specify user")
297                         }
298
299                         if err := msg.From().Ignored.Remove(id); err != nil {
300                                 return err
301                         }
302
303                         room.Send(message.NewSystemMsg(fmt.Sprintf("No longer ignoring: %s", id), msg.From()))
304                         return nil
305                 },
306         })
307 }