File Coverage

blib/lib/Alice/Role/Commands.pm
Criterion Covered Total %
statement 16 55 29.0
branch 1 24 4.1
condition 0 6 0.0
subroutine 5 12 41.6
pod 0 5 0.0
total 22 102 21.5


line stmt bran cond sub pod time code
1             package Alice::Role::Commands;
2              
3 2     2   1691 use Any::Moose 'Role';
  2         7  
  2         23  
4              
5 2     2   5508 use List::MoreUtils qw/none/;
  2         6  
  2         121  
6 2     2   12 use Try::Tiny;
  2         6  
  2         131  
7 2         11 use Class::Throwable qw/NetworkRequired InvalidNetwork ChannelRequired
8 2     2   1977 InvalidArguments UnknownCommand/;
  2         3286  
9              
10             our %COMMANDS;
11             my $SRVOPT = qr/\-(\S+)\s*/;
12              
13             sub commands {
14 0     0 0 0 return grep {$_->{eg}} values %COMMANDS;
  0         0  
15             }
16              
17             sub irc_command {
18 0     0 0 0 my ($self, $req) = @_;
19              
20             try {
21 0     0   0 my ($command, $args) = $self->match_irc_command($req->line);
22 0 0       0 if ($command) {
23 0         0 $self->run_irc_command($command, $req, $args);
24             }
25             else {
26 0         0 throw UnknownCommand $req->line ." does not match any known commands. Try /help";
27             }
28             }
29             catch {
30 0     0   0 $req->reply("$_");
31             }
32 0         0 }
33              
34             sub match_irc_command {
35 0     0 0 0 my ($self, $line) = @_;
36              
37 0 0       0 $line = "/say $line" unless substr($line, 0, 1) eq "/";
38              
39 0         0 for my $name (keys %COMMANDS) {
40              
41 0 0       0 if ($line =~ m{^/$name\b\s*(.*)}) {
42 0         0 my $args = $1;
43 0         0 return ($name, $args);
44             }
45             }
46             }
47              
48             sub run_irc_command {
49 0     0 0 0 my ($self, $name, $req, $args) = @_;
50 0         0 my $command = $COMMANDS{$name};
51 0         0 my $opts = [];
52              
53             # must be in a channel
54 0         0 my $type = $req->window->type;
55 0 0 0 0   0 if ($command->{window_type} and none {$_ eq $type} @{$command->{window_type}}) {
  0         0  
  0         0  
56 0         0 my $types = join " or ", @{$command->{window_type}};
  0         0  
57 0         0 throw ChannelRequired "Must be in a $types for /$command->{name}.";
58             }
59              
60 0         0 my $network = $req->window->network;
61              
62             # determine the network can be overridden
63 0 0 0     0 if ($command->{network} and $args =~ s/^$SRVOPT//) {
64 0         0 $network = $1;
65             }
66              
67             # command requires a connected network
68 0 0       0 if ($command->{connection}) {
69 0 0       0 throw NetworkRequired $command->{eg} unless $network;
70              
71 0         0 my $irc = $self->get_irc($network);
72              
73 0 0       0 throw InvalidNetwork "The $network network does not exist."
74             unless $irc;
75              
76 0 0       0 throw InvalidNetwork "The $network network is not connected"
77             unless $irc->is_connected;
78              
79 0         0 $req->irc($irc);
80             }
81              
82             # gather any options
83 0 0       0 if (my $opt_re = $command->{opts}) {
84 0 0       0 if (my (@opts) = ($args =~ /$opt_re/)) {
85 0         0 $opts = \@opts;
86             }
87             else {
88 0         0 throw InvalidArguments $command->{eg};
89             }
90             }
91              
92 0         0 $command->{cb}->($self, $req, $opts);
93             }
94              
95             sub command {
96 46     46 0 74 my ($name, $opts) = @_;
97              
98 46 50       88 if ($opts) {
99 46         112 $COMMANDS{$name} = $opts;
100             }
101              
102 46         73 return $COMMANDS{$name};
103             }
104              
105             command say => {
106             name => "say",
107             window_type => [qw/channel privmsg/],
108             connection => 1,
109             eg => "/SAY ",
110             opts => qr{(.+)},
111             cb => sub {
112             my ($self, $req, $opts) = @_;
113              
114             my $msg = $opts->[0];
115             $self->send_message($req->window, $req->irc->nick, $msg);
116             $req->irc->send_long_line(PRIVMSG => $req->window->title, $msg);
117             },
118             };
119              
120             command msg => {
121             name => "msg",
122             opts => qr{(\S+)\s*(.*)},
123             eg => "/MSG [-] []",
124             desc => "Sends a message to a nick.",
125             connection => 1,
126             network => 1,
127             cb => sub {
128             my ($self, $req, $opts) = @_;
129              
130             my ($nick, $msg) = @$opts;
131              
132             my $new_window = $self->find_or_create_window($nick, $req->irc);
133             $self->broadcast($new_window->join_action);
134              
135             if ($msg) {
136             $self->send_message($new_window, $req->nick, $msg);
137             $req->send_srv(PRIVMSG => $nick, $msg);
138             }
139             }
140             };
141              
142             command nick => {
143             name => "nick",
144             opts => qr{(\S+)},
145             connection => 1,
146             network => 1,
147             eg => "/NICK [-] ",
148             desc => "Changes your nick.",
149             cb => sub {
150             my ($self, $req, $opts) = @_;
151              
152             my $nick = $opts->[0];
153              
154             $req->reply("changing nick to $nick on " . $req->irc->name);
155             $req->irc->send_srv(NICK => $nick);
156             }
157             };
158              
159             command qr{names|n} => {
160             name => "names",
161             window_type => [qw/channel/],
162             connection => 1,
163             eg => "/NAMES [-avatars]",
164             desc => "Lists nicks in current channel.",
165             cb => sub {
166             my ($self, $req) = @_;
167             my @nicks = $req->irc->channel_nicks($req->window->title);
168             $req->reply($req->window->nick_table(@nicks));
169             },
170             };
171              
172             command qr{join|j} => {
173             name => "join",
174             opts => qr{(\S+)\s*(\S+)?},
175             connection => 1,
176             network => 1,
177             eg => "/JOIN [-] []",
178             desc => "Joins the specified channel.",
179             cb => sub {
180             my ($self, $req, $opts) = @_;
181              
182             my $channel = $opts->[0];
183             $req->reply("joining $channel on ". $req->irc->name);
184             $req->send_srv(JOIN => @$opts);
185             },
186             };
187              
188             command create => {
189             name => "create",
190             opts => qr{(\S+)},
191             connection => 1,
192             network => 1,
193             cb => sub {
194             my ($self, $req, $opts) = @_;
195              
196             my $name = $opts->[0];
197              
198             my $new_window = $self->find_or_create_window($name, $req->irc);
199             $self->broadcast($new_window->join_action);
200             }
201             };
202              
203             command qr{close|wc|part} => {
204             name => 'part',
205             window_type => [qw/channel privmsg/],
206             eg => "/PART",
207             network => 1,
208             desc => "Leaves and closes the focused window.",
209             cb => sub {
210             my ($self, $req) = @_;
211             my $window = $req->window;
212              
213             $self->close_window($window);
214             my $irc = $self->get_irc($window->network);
215              
216             if ($window->is_channel and $irc->is_connected) {
217             $irc->send_srv(PART => $window->title);
218             }
219             },
220             };
221              
222             command clear => {
223             name => 'clear',
224             eg => "/CLEAR",
225             desc => "Clears lines from current window.",
226             cb => sub {
227             my ($self, $req) = @_;
228             $req->window->buffer->clear;
229             $self->broadcast($req->window->clear_action);
230             },
231             };
232              
233             command qr{topic|t} => {
234             name => 'topic',
235             opts => qr{(.+)?},
236             window_type => ['channel'],
237             connection => 1,
238             network => 1,
239             eg => "/TOPIC []",
240             desc => "Shows and/or changes the topic of the current channel.",
241             cb => sub {
242             my ($self, $req, $opts) = @_;
243              
244             my $new_topic = $opts->[0];
245              
246             if ($new_topic) {
247             $req->window->topic({string => $new_topic, nick => $req->nick, time => time});
248             $req->send_srv(TOPIC => $req->window->title, $new_topic);
249             }
250             else {
251             $req->stream->send($req->window->format_topic);
252             }
253             }
254             };
255              
256             command whois => {
257             name => 'whois',
258             connection => 1,
259             network => 1,
260             opts => qr{(\S+)},
261             eg => "/WHOIS [-] ",
262             desc => "Shows info about the specified nick",
263             cb => sub {
264             my ($self, $req, $opts) = @_;
265              
266             my $nick = $opts->[0];
267             my $irc = $req->irc;
268              
269             $irc->add_whois($nick,sub {
270             $req->reply($_[0] ? $_[0] : "No such nick: $nick on " . $irc->name);
271             });
272             }
273             };
274              
275             command me => {
276             name => 'me',
277             opts => qr{(.+)},
278             eg => "/ME ",
279             window_type => [qw/channel privmsg/],
280             connection => 1,
281             desc => "Sends a CTCP ACTION to the current window.",
282             cb => sub {
283             my ($self, $req, $opts) = @_;
284             my $action = $opts->[0];
285              
286             $self->send_message($req->window, $req->nick, "\x{2022} $action");
287             $action = AnyEvent::IRC::Util::encode_ctcp(["ACTION", $action]);
288             $req->send_srv(PRIVMSG => $req->window->title, $action);
289             },
290             };
291              
292             command quote => {
293             name => 'quote',
294             opts => qr{(.+)},
295             connection => 1,
296             network => 1,
297             eg => "/QUOTE [-] ",
298             desc => "Sends the server raw data without parsing.",
299             cb => sub {
300             my ($self, $req, $opts) = @_;
301             $req->irc->send_raw($opts->[0]);
302             },
303             };
304              
305             command disconnect => {
306             name => 'disconnect',
307             opts => qr{(\S+)},
308             eg => "/DISCONNECT ",
309             desc => "Disconnects from the specified server.",
310             cb => sub {
311             my ($self, $req, $opts) = @_;
312             my $network = $opts->[0];
313             $self->disconnect_irc($network);
314             },
315             };
316              
317             command 'connect' => {
318             name => 'connect',
319             opts => qr{(\S+)},
320             eg => "/CONNECT ",
321             desc => "Connects to the specified server.",
322             cb => sub {
323             my ($self, $req, $opts) = @_;
324             my $network = $opts->[0];
325             $self->connect_irc($network);
326             }
327             };
328              
329             command ignore => {
330             name => 'ignore',
331             opts => qr{(\S+)\s*(\S+)?},
332             eg => "/IGNORE [] ",
333             desc => "Adds a nick or channel to ignore list. Types include 'msg', 'part', 'join'. Defaults to 'msg'.",
334             cb => sub {
335             my ($self, $req, $opts) = @_;
336            
337             if (!$opts->[1]) {
338             unshift @$opts, "msg";
339             }
340              
341             my ($type, $nick) = @$opts;
342              
343             $self->add_ignore($type, $nick);
344             $req->reply("Ignoring $type from $nick");
345             },
346             };
347              
348             command unignore => {
349             name => 'unignore',
350             opts => qr{(\S+)\s*(\S+)?},
351             eg => "/UNIGNORE [] ",
352             desc => "Removes nick from ignore list. Types include 'msg', 'part', 'join'. Defaults to 'msg'.",
353             cb => sub {
354             my ($self, $req, $opts) = @_;
355            
356             if (!$opts->[1]) {
357             unshift @$opts, "msg";
358             }
359              
360             my ($type, $nick) = @$opts;
361              
362             $self->remove_ignore($type, $nick);
363             $req->reply("No longer ignoring $nick");
364             },
365             };
366              
367             command ignores => {
368             name => 'ignores',
369             eg => "/IGNORES",
370             desc => "Lists ignored nicks.",
371             cb => sub {
372             my ($self, $req) = @_;
373              
374             my $msg;
375              
376             for my $type(qw/msg part join/) {
377             $msg .= "$type: ";
378             $msg .= (join ", ", $self->ignores($type)) || "none";
379             $msg .= "\n";
380             }
381              
382             $req->reply("Ignoring\n$msg");
383             },
384             };
385              
386             command qr{window|w} => {
387             name => 'window',
388             opts => qr{(\d+|next|prev(?:ious)?)},
389             eg => "/WINDOW ",
390             desc => "Focuses the provided window number",
391             cb => sub {
392             my ($self, $req, $opts) = @_;
393            
394             $req->stream->send({
395             type => "action",
396             event => "focus",
397             window_number => $opts->[0],
398             });
399             }
400             };
401              
402             command away => {
403             name => 'away',
404             opts => qr{(.+)?},
405             eg => "/AWAY []",
406             desc => "Set or remove an away message",
407             cb => sub {
408             my ($self, $req, $opts) = @_;
409              
410             if (my $message = $opts->[0]) {
411             $req->reply("Setting away status: $message");
412             $self->set_away($message);
413             }
414             else {
415             $req->reply("Removing away status.");
416             $self->set_away;
417             }
418             }
419             };
420              
421             command invite => {
422             name => 'invite',
423             connection => 1,
424             opts => qr{(\S+)\s+(\S+)},
425             eg => "/INVITE ",
426             desc => "Invite a user to a channel you're in",
427             cb => sub {
428             my ($self, $req, $opts) = @_;
429              
430             my ($nick, $channel) = @$opts;
431              
432             $req->reply("Inviting $nick to $channel");
433             $req->send_srv(INVITE => $nick, $channel);
434             },
435             };
436              
437             command help => {
438             name => 'help',
439             eg => "/HELP []",
440             desc => "Shows list of commands or overview of a specific command.",
441             opts => qr{(\S+)?},
442             cb => sub {
443             my ($self, $req, $opts) = @_;
444              
445             my $command = $opts->[0];
446              
447             if (!$command) {
448             my $commands = join " ", map {uc $_->{name}} grep {$_->{eg}} values %COMMANDS;
449             $req->reply('/HELP for help with a specific command');
450             $req->reply("Available commands: $commands");
451             return;
452             }
453              
454             for (values %COMMANDS) {
455             if ($_->{name} eq lc $command) {
456             $req->reply("$_->{eg}\n$_->{desc}");
457             return;
458             }
459             }
460              
461             $req->reply("No help for ".uc $command);
462             }
463             };
464              
465             command chunk => {
466             name => 'chunk',
467             opts => qr{(\d+) (\d+)},
468             cb => sub {
469             my ($self, $req, $opts) = @_;
470             my $window = $req->window;
471              
472             $self->update_window($req->stream, $window, $opts->[1], 0, $opts->[0], 0);
473             }
474             };
475              
476             command trim => {
477             name => 'trim',
478             eg => "/TRIM []",
479             desc => "Trims the current tab to of lines. Defaults to 50.",
480             window => 1,
481             opts => qr{(\d+)?},
482             cb => sub {
483             my ($self, $req, $opts) = @_;
484             my $lines = $opts->[0] || 50;
485             $req->stream->send($req->window->trim_action($lines));
486             }
487             };
488              
489             1;