source: skype/t/irssi/trigger.pl @ 3512d0b

Last change on this file since 3512d0b was 25a7eb8, checked in by Miklos Vajna <vmiklos@…>, at 2009-02-17T23:31:13Z

Initial testcase.

Code is based on ulim's automated live tests.

  • Property mode set to 100644
File size: 40.6 KB
RevLine 
[25a7eb8]1# trigger.pl - execute a command or replace text, triggered by an event in irssi
2# Do /TRIGGER HELP or look at http://wouter.coekaerts.be/irssi/ for help
3
4# Copyright (C) 2002-2006  Wouter Coekaerts <wouter@coekaerts.be>
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
20use strict;
21use Irssi 20020324 qw(command_bind command_runsub command signal_add_first signal_continue signal_stop signal_remove);
22use Text::ParseWords;
23use IO::File;
24use vars qw($VERSION %IRSSI);
25
26$VERSION = '1.0';
27%IRSSI = (
28        authors     => 'Wouter Coekaerts',
29        contact     => 'wouter@coekaerts.be',
30        name        => 'trigger',
31        description => 'execute a command or replace text, triggered by an event in irssi',
32        license     => 'GPLv2 or later',
33        url         => 'http://wouter.coekaerts.be/irssi/',
34        changed     => '$LastChangedDate: 2006-01-23 13:10:19 +0100 (Mon, 23 Jan 2006) $',
35);
36
37sub cmd_help {
38        Irssi::print (<<'SCRIPTHELP_EOF', MSGLEVEL_CLIENTCRAP);
39
40TRIGGER LIST
41TRIGGER SAVE
42TRIGGER RELOAD
43TRIGGER MOVE <number> <number>
44TRIGGER DELETE <number>
45TRIGGER CHANGE <number> ...
46TRIGGER ADD ...
47
48When to match:
49On which types of event to trigger:
50     These are simply specified by -name_of_the_type
51     The normal IRC event types are:
52          publics, %|privmsgs, pubactions, privactions, pubnotices, privnotices, joins, parts, quits, kicks, topics, invites, nick_changes, dcc_msgs, dcc_actions, dcc_ctcps
53          mode_channel: %|a mode on the (whole) channel (like +t, +i, +b)
54          mode_nick: %|a mode on someone in the channel (like +o, +v)
55     -all is an alias for all of those.
56     Additionally, there is:
57          rawin: %|raw text incoming from the server
58          send_command: %|commands you give to irssi
59          send_text: %|lines you type that aren't commands
60          beep: %|when irssi beeps
61          notify_join: %|someone in you notify list comes online
62          notify_part: %|someone in your notify list goes offline
63          notify_away: %|someone in your notify list goes away
64          notify_unaway: %|someone in your notify list goes unaway
65          notify_unidle: %|someone in your notify list stops idling
66
67Filters (conditions) the event has to satisfy. They all take one parameter.
68If you can give a list, seperate elements by space and use quotes around the list.
69     -pattern: %|The message must match the given pattern. ? and * can be used as wildcards
70     -regexp: %|The message must match the given regexp. (see man perlre)
71       %|if -nocase is given as an option, the regexp or pattern is matched case insensitive
72     -tags: %|The servertag must be in the given list of tags
73     -channels: %|The event must be in one of the given list of channels.
74                Examples: %|-channels '#chan1 #chan2' or -channels 'IRCNet/#channel'
75                          %|-channels 'EFNet/' means every channel on EFNet and is the same as -tags 'EFNet'
76     -masks: %|The person who triggers it must match one of the given list of masks
77     -hasmode: %|The person who triggers it must have the give mode
78               Examples: %|'-o' means not opped, '+ov' means opped OR voiced, '-o&-v' means not opped AND not voiced
79     -hasflag: %|Only trigger if if friends.pl (friends_shasta.pl) or people.pl is loaded and the person who triggers it has the given flag in the script (same syntax as -hasmode)
80     -other_masks
81     -other_hasmode
82     -other_hasflag: %|Same as above but for the victim for kicks or mode_nick.
83
84What to do when it matches:
85     -command: Execute the given Irssi-command
86                %|You are able to use $1, $2 and so on generated by your regexp pattern.
87                %|For multiple commands ; (or $;) can be used as seperator
88                %|The following variables are also expanded:
89                   $T: %|Server tag
90                   $C: %|Channel name
91                   $N: %|Nickname of the person who triggered this command
92                   $A: %|His address (foo@bar.com),
93                   $I: %|His ident (foo)
94                   $H: %|His hostname (bar.com)
95                   $M: %|The complete message
96                   ${other}: %|The victim for kicks or mode_nick
97                   ${mode_type}: %|The type ('+' or '-') for a mode_channel or mode_nick
98                   ${mode_char}: %|The mode char ('o' for ops, 'b' for ban,...)
99                   ${mode_arg} : %|The argument to the mode (if there is one)
100                %|$\X, with X being one of the above expands (e.g. $\M), escapes all non-alphanumeric characters, so it can be used with /eval or /exec. Don't use /eval or /exec without this, it's not safe.
101
102     -replace: %|replaces the matching part with the given replacement in the event (requires a -regexp or -pattern)
103     -once: %|remove the trigger if it is triggered, so it only executes once and then is forgotten.
104     -stop: %|stops the signal. It won't get displayed by Irssi. Like /IGNORE
105     -debug: %|print some debugging info
106
107Other options:
108     -disabled: %|Same as removing it, but keeps it in case you might need it later
109     -name: %|Give the trigger a name. You can refer to the trigger with this name in add/del/change commands
110
111Examples:
112 Knockout people who do a !list:
113   /TRIGGER ADD %|-publics -channels "#channel1 #channel2" -nocase -regexp ^!list -command "KN $N This is not a warez channel!"
114 React to !echo commands from people who are +o in your friends-script:
115   /TRIGGER ADD %|-publics -regexp '^!echo (.*)' -hasflag '+o' -command 'say echo: $1'
116 Ignore all non-ops on #channel:
117   /TRIGGER ADD %|-publics -actions -channels "#channel" -hasmode '-o' -stop
118 Send a mail to yourself every time a topic is changed:
119   /TRIGGER ADD %|-topics -command 'exec echo $\N changed topic of $\C to: $\M | mail you@somewhere.com -s topic'
120
121
122Examples with -replace:
123 %|Replace every occurence of shit with sh*t, case insensitive:
124   /TRIGGER ADD %|-all -nocase -regexp shit -replace sh*t
125 %|Strip all colorcodes from *!lamer@*:
126   /TRIGGER ADD %|-all -masks *!lamer@* -regexp '\x03\d?\d?(,\d\d?)?|\x02|\x1f|\x16|\x06' -replace ''
127 %|Never let *!bot1@foo.bar or *!bot2@foo.bar hilight you
128 %|(this works by cutting your nick in 2 different parts, 'myn' and 'ick' here)
129 %|you don't need to understand the -replace argument, just trust that it works if the 2 parts separately don't hilight:
130   /TRIGGER ADD %|-all masks '*!bot1@foo.bar *!bot2@foo.bar' -regexp '(myn)(ick)' -nocase -replace '$1\x02\x02$2'
131 %|Avoid being hilighted by !top10 in eggdrops with stats.mod (but show your nick in bold):
132   /TRIGGER ADD %|-publics -regexp '(Top.0\(.*\): 1.*)(my)(nick)' -replace '$1\x02$2\x02\x02$3\x02'
133 %|Convert a Windows-1252 Euro to an ISO-8859-15 Euro (same effect as euro.pl):
134   /TRIGGER ADD %|-regexp '\x80' -replace '\xA4'
135 %|Show tabs as spaces, not the inverted I (same effect as tab_stop.pl):
136   /TRIGGER ADD %|-all -regexp '\t' -replace '    '
137SCRIPTHELP_EOF
138} # /
139
140my @triggers; # array of all triggers
141my %triggers_by_type; # hash mapping types on triggers of that type
142my $recursion_depth = 0;
143my $changed_since_last_save = 0;
144
145###############
146### formats ###
147###############
148
149Irssi::theme_register([
150        'trigger_header' => 'Triggers:',
151        'trigger_line' => '%#$[-4]0 $1',
152        'trigger_added' => 'Trigger $0 added: $1',
153        'trigger_not_found' => 'Trigger {hilight $0} not found',
154        'trigger_saved' => 'Triggers saved to $0',
155        'trigger_loaded' => 'Triggers loaded from $0'
156]);
157
158#########################################
159### catch the signals & do your thing ###
160#########################################
161
162# trigger types with a message and a channel
163my @allchanmsg_types = qw(publics pubactions pubnotices pubctcps pubctcpreplies parts quits kicks topics);
164# trigger types with a message
165my @allmsg_types = (@allchanmsg_types, qw(privmsgs privactions privnotices privctcps privctcpreplies dcc_msgs dcc_actions dcc_ctcps));
166# trigger types with a channel
167my @allchan_types = (@allchanmsg_types, qw(mode_channel mode_nick joins invites));
168# trigger types in -all
169my @all_types = (@allmsg_types, qw(mode_channel mode_nick joins invites nick_changes));
170# trigger types with a server
171my @all_server_types = (@all_types, qw(rawin notify_join notify_part notify_away notify_unaway notify_unidle));
172# all trigger types
173my @trigger_types = (@all_server_types, qw(send_command send_text beep));
174#trigger types that are not in -all
175#my @notall_types = grep {my $a=$_; return (!grep {$_ eq $a} @all_types);} @trigger_types;
176my @notall_types = qw(rawin notify_join notify_part notify_away notify_unaway notify_unidle send_command send_text beep);
177
178my @signals = (
179# "message public", SERVER_REC, char *msg, char *nick, char *address, char *target
180{
181        'types' => ['publics'],
182        'signal' => 'message public',
183        'sub' => sub {check_signal_message(\@_,1,$_[0],$_[4],$_[2],$_[3],'publics');},
184},
185# "message private", SERVER_REC, char *msg, char *nick, char *address
186{
187        'types' => ['privmsgs'],
188        'signal' => 'message private',
189        'sub' => sub {check_signal_message(\@_,1,$_[0],undef,$_[2],$_[3],'privmsgs');},
190},
191# "message irc action", SERVER_REC, char *msg, char *nick, char *address, char *target
192{
193        'types' => ['privactions','pubactions'],
194        'signal' => 'message irc action',
195        'sub' => sub {
196                if ($_[4] eq $_[0]->{nick}) {
197                        check_signal_message(\@_,1,$_[0],undef,$_[2],$_[3],'privactions');
198                } else {
199                        check_signal_message(\@_,1,$_[0],$_[4],$_[2],$_[3],'pubactions');
200                }
201        },
202},
203# "message irc notice", SERVER_REC, char *msg, char *nick, char *address, char *target
204{
205        'types' => ['privnotices','pubnotices'],
206        'signal' => 'message irc notice',
207        'sub' => sub {
208                if ($_[4] eq $_[0]->{nick}) {
209                        check_signal_message(\@_,1,$_[0],undef,$_[2],$_[3],'privnotices');
210                } else {
211                        check_signal_message(\@_,1,$_[0],$_[4],$_[2],$_[3],'pubnotices');
212                }
213        }
214},
215# "message join", SERVER_REC, char *channel, char *nick, char *address
216{
217        'types' => ['joins'],
218        'signal' => 'message join',
219        'sub' => sub {check_signal_message(\@_,-1,$_[0],$_[1],$_[2],$_[3],'joins');}
220},
221# "message part", SERVER_REC, char *channel, char *nick, char *address, char *reason
222{
223        'types' => ['parts'],
224        'signal' => 'message part',
225        'sub' => sub {check_signal_message(\@_,4,$_[0],$_[1],$_[2],$_[3],'parts');}
226},
227# "message quit", SERVER_REC, char *nick, char *address, char *reason
228{
229        'types' => ['quits'],
230        'signal' => 'message quit',
231        'sub' => sub {check_signal_message(\@_,3,$_[0],undef,$_[1],$_[2],'quits');}
232},
233# "message kick", SERVER_REC, char *channel, char *nick, char *kicker, char *address, char *reason
234{
235        'types' => ['kicks'],
236        'signal' => 'message kick',
237        'sub' => sub {check_signal_message(\@_,5,$_[0],$_[1],$_[3],$_[4],'kicks',{'other'=>$_[2]});}
238},
239# "message topic", SERVER_REC, char *channel, char *topic, char *nick, char *address
240{
241        'types' => ['topics'],
242        'signal' => 'message topic',
243        'sub' => sub {check_signal_message(\@_,2,$_[0],$_[1],$_[3],$_[4],'topics');}
244},
245# "message invite", SERVER_REC, char *channel, char *nick, char *address
246{
247        'types' => ['invites'],
248        'signal' => 'message invite',
249        'sub' => sub {check_signal_message(\@_,-1,$_[0],$_[1],$_[2],$_[3],'invites');}
250},
251# "message nick", SERVER_REC, char *newnick, char *oldnick, char *address
252{
253        'types' => ['nick_changes'],
254        'signal' => 'message nick',
255        'sub' => sub {check_signal_message(\@_,-1,$_[0],undef,$_[1],$_[3],'nick_changes');}
256},
257# "message dcc", DCC_REC *dcc, char *msg
258{
259        'types' => ['dcc_msgs'],
260        'signal' => 'message dcc',
261        'sub' => sub {check_signal_message(\@_,1,$_[0]->{'server'},undef,$_[0]->{'nick'},undef,'dcc_msgs');
262        }
263},
264# "message dcc action", DCC_REC *dcc, char *msg
265{
266        'types' => ['dcc_actions'],
267        'signal' => 'message dcc action',
268        'sub' => sub {check_signal_message(\@_,1,$_[0]->{'server'},undef,$_[0]->{'nick'},undef,'dcc_actions');}
269},
270# "message dcc ctcp", DCC_REC *dcc, char *cmd, char *data
271{
272        'types' => ['dcc_ctcps'],
273        'signal' => 'message dcc ctcp',
274        'sub' => sub {check_signal_message(\@_,1,$_[0]->{'server'},undef,$_[0]->{'nick'},undef,'dcc_ctcps');}
275},
276# "server incoming", SERVER_REC, char *data
277{
278        'types' => ['rawin'],
279        'signal' => 'server incoming',
280        'sub' => sub {check_signal_message(\@_,1,$_[0],undef,undef,undef,'rawin');}
281},
282# "send command", char *args, SERVER_REC, WI_ITEM_REC
283{
284        'types' => ['send_command'],
285        'signal' => 'send command',
286        'sub' => sub {
287                sig_send_text_or_command(\@_,1);
288        }
289},
290# "send text", char *line, SERVER_REC, WI_ITEM_REC
291{
292        'types' => ['send_text'],
293        'signal' => 'send text',
294        'sub' => sub {
295                sig_send_text_or_command(\@_,0);
296        }
297},
298# "beep"
299{
300        'types' => ['beep'],
301        'signal' => 'beep',
302        'sub' => sub {check_signal_message(\@_,-1,undef,undef,undef,undef,'beep');}
303},
304# "event "<cmd>, SERVER_REC, char *args, char *sender_nick, char *sender_address
305{
306        'types' => ['mode_channel', 'mode_nick'],
307        'signal' => 'event mode',
308        'sub' => sub {
309                my ($server, $event_args, $nickname, $address) = @_;
310                my ($target, $modes, $modeargs) = split(/ /, $event_args, 3);
311                return if (!$server->ischannel($target));
312                my (@modeargs) = split(/ /,$modeargs);
313                my ($pos, $type, $event_type, $arg) = (0, '+');
314                foreach my $char (split(//,$modes)) {
315                        if ($char eq "+" || $char eq "-") {
316                                $type = $char;
317                        } else {
318                                if ($char =~ /[Oovh]/) { # mode_nick
319                                        $event_type = 'mode_nick';
320                                        $arg = $modeargs[$pos++];
321                                } elsif ($char =~ /[beIqdk]/ || ( $char =~ /[lfJ]/ && $type eq '+')) { # chan_mode with arg
322                                        $event_type = 'mode_channel';
323                                        $arg = $modeargs[$pos++];
324                                } else { # chan_mode without arg
325                                        $event_type = 'mode_channel';
326                                        $arg = undef;
327                                }
328                                check_signal_message(\@_,-1,$server,$target,$nickname,$address,$event_type,{
329                                        'mode_type' => $type,
330                                        'mode_char' => $char,
331                                        'mode_arg' => $arg,
332                                        'other' => ($event_type eq 'mode_nick') ? $arg : undef
333                                });
334                        }
335                }
336        }
337},
338# "notifylist joined", SERVER_REC, char *nick, char *user, char *host, char *realname, char *awaymsg
339{
340        'types' => ['notify_join'],
341        'signal' => 'notifylist joined',
342        'sub' => sub {check_signal_message(\@_, 5, $_[0], undef, $_[1], $_[2].'@'.$_[3], 'notify_join', {'realname' => $_[4]});}
343},
344{
345        'types' => ['notify_part'],
346        'signal' => 'notifylist left',
347        'sub' => sub {check_signal_message(\@_, 5, $_[0], undef, $_[1], $_[2].'@'.$_[3], 'notify_left', {'realname' => $_[4]});}
348},
349{
350        'types' => ['notify_unidle'],
351        'signal' => 'notifylist unidle',
352        'sub' => sub {check_signal_message(\@_, 5, $_[0], undef, $_[1], $_[2].'@'.$_[3], 'notify_unidle', {'realname' => $_[4]});}
353},
354{
355        'types' => ['notify_away', 'notify_unaway'],
356        'signal' => 'notifylist away changed',
357        'sub' => sub {check_signal_message(\@_, 5, $_[0], undef, $_[1], $_[2].'@'.$_[3], ($_[5] ? 'notify_away' : 'notify_unaway'), {'realname' => $_[4]});}
358},
359# "ctcp msg", SERVER_REC, char *args, char *nick, char *addr, char *target
360{
361        'types' => ['pubctcps', 'privctcps'],
362        'signal' => 'ctcp msg',
363        'sub' => sub {
364                my ($server, $args, $nick, $addr, $target) = @_;
365                if ($target eq $server->{'nick'}) {
366                        check_signal_message(\@_, 1, $server, undef, $nick, $addr, 'privctcps');
367                } else {
368                        check_signal_message(\@_, 1, $server, $target, $nick, $addr, 'pubctcps');
369                }
370        }
371},
372# "ctcp reply", SERVER_REC, char *args, char *nick, char *addr, char *target
373{
374        'types' => ['pubctcpreplies', 'privctcpreplies'],
375        'signal' => 'ctcp reply',
376        'sub' => sub {
377                my ($server, $args, $nick, $addr, $target) = @_;
378                if ($target eq $server->{'nick'}) {
379                        check_signal_message(\@_, 1, $server, undef, $nick, $addr, 'privctcps');
380                } else {
381                        check_signal_message(\@_, 1, $server, $target, $nick, $addr, 'pubctcps');
382                }
383        }
384}
385);
386
387sub sig_send_text_or_command {
388        my ($signal, $iscommand) = @_;
389        my ($line, $server, $item) = @$signal;
390        my ($channelname,$nickname,$address) = (undef,undef,undef);
391        if ($item && (ref($item) eq 'Irssi::Irc::Channel' || ref($item) eq 'Irssi::Silc::Channel')) {
392                $channelname = $item->{'name'};
393        } elsif ($item && ref($item) eq 'Irssi::Irc::Query') { # TODO Silc query ?
394                $nickname = $item->{'name'};
395                $address = $item->{'address'}
396        }
397        # TODO pass context also for non-channels (queries and other stuff)
398        check_signal_message($signal,0,$server,$channelname,$nickname,$address,$iscommand ? 'send_command' : 'send_text');
399
400}
401
402my %filters = (
403'tags' => {
404        'types' => \@all_server_types,
405        'sub' => sub {
406                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
407               
408                if (!defined($server)) {
409                        return 0;
410                }
411                my $matches = 0;
412                foreach my $tag (split(/ /,$param)) {
413                        if (lc($server->{'tag'}) eq lc($tag)) {
414                                $matches = 1;
415                                last;
416                        }
417                }
418                return $matches;
419        }
420},
421'channels' => {
422        'types' => \@allchan_types,
423        'sub' => sub {
424                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
425               
426                if (!defined($channelname) || !defined($server)) {
427                        return 0;
428                }
429                my $matches = 0;
430                foreach my $trigger_channel (split(/ /,$param)) {
431                        if (lc($channelname) eq lc($trigger_channel)
432                                || lc($server->{'tag'}.'/'.$channelname) eq lc($trigger_channel)
433                                || lc($server->{'tag'}.'/') eq lc($trigger_channel)) {
434                                $matches = 1;
435                                last; # this channel matches, stop checking channels
436                        }
437                }
438                return $matches;
439        }
440},
441'masks' => {
442        'types' => \@all_types,
443        'sub' => sub {
444                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
445                return  (defined($nickname) && defined($address) && defined($server) && $server->masks_match($param, $nickname, $address));
446        }
447},
448'other_masks' => {
449        'types' => ['kicks', 'mode_nick'],
450        'sub' => sub {
451                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
452                return 0 unless defined($extra->{'other'});
453                my $other_address = get_address($extra->{'other'}, $server, $channelname);
454                return defined($other_address) && $server->masks_match($param, $extra->{'other'}, $other_address);
455        }
456},
457'hasmode' => {
458        'types' => \@all_types,
459        'sub' => sub {
460                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
461                return hasmode($param, $nickname, $server, $channelname);
462        }
463},
464'other_hasmode' => {
465        'types' => ['kicks', 'mode_nick'],
466        'sub' => sub {
467                my ($param,$signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
468                return defined($extra->{'other'}) && hasmode($param, $extra->{'other'}, $server, $channelname);
469        }
470},
471'hasflag' => {
472        'types' => \@all_types,
473        'sub' => sub {
474                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
475                return 0 unless defined($nickname) && defined($address) && defined($server);
476                my $flags = get_flags ($server->{'chatnet'},$channelname,$nickname,$address);
477                return defined($flags) && check_modes($flags,$param);
478        }
479},
480'other_hasflag' => {
481        'types' => ['kicks', 'mode_nick'],
482        'sub' => sub {
483                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
484                return 0 unless defined($extra->{'other'});
485                my $other_address = get_address($extra->{'other'}, $server, $channelname);
486                return 0 unless defined($other_address);
487                my $flags = get_flags ($server->{'chatnet'},$channelname,$extra->{'other'},$other_address);
488                return defined($flags) && check_modes($flags,$param);
489        }
490},
491'mode_type' => {
492        'types' => ['mode_channel', 'mode_nick'],
493        'sub' => sub {
494                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
495                return (($param) eq $extra->{'mode_type'});
496        }
497},
498'mode_char' => {
499        'types' => ['mode_channel', 'mode_nick'],
500        'sub' => sub {
501                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
502                return (($param) eq $extra->{'mode_char'});
503        }
504},
505'mode_arg' => {
506        'types' => ['mode_channel', 'mode_nick'],
507        'sub' => sub {
508                my ($param, $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra) = @_;
509                return (($param) eq $extra->{'mode_arg'});
510        }
511}
512);
513
514sub get_address {
515        my ($nick, $server, $channel) = @_;
516        my $nickrec = get_nickrec($nick, $server, $channel);
517        return $nickrec ? $nickrec->{'host'} : undef;
518}
519sub get_nickrec {
520        my ($nick, $server, $channel) = @_;
521        return unless defined($server) && defined($channel) && defined($nick);
522        my $chanrec = $server->channel_find($channel);
523        return $chanrec ? $chanrec->nick_find($nick) : undef;
524}
525
526sub hasmode {
527        my ($param, $nickname, $server, $channelname) = @_;
528        my $nickrec = get_nickrec($nickname, $server, $channelname);
529        return 0 unless defined $nickrec;
530        my $modes =
531                ($nickrec->{'op'} ? 'o' : '')
532                . ($nickrec->{'voice'} ? 'v' : '')
533                . ($nickrec->{'halfop'} ? 'h' : '')
534        ;
535        return check_modes($modes, $param);
536}
537
538# list of all switches
539my @trigger_switches = (@trigger_types, qw(all nocase stop once debug disabled));
540# parameters (with an argument)
541my @trigger_params = qw(pattern regexp command replace name);
542# list of all options (including switches) for /TRIGGER ADD
543my @trigger_add_options = (@trigger_switches, @trigger_params, keys(%filters));
544# same for /TRIGGER CHANGE, this includes the -no<option>'s
545my @trigger_options = map(($_,'no'.$_) ,@trigger_add_options);
546
547# check the triggers on $signal's $parammessage parameter, for triggers with $condition set
548#  on $server in $channelname, for $nickname!$address
549# set $parammessage to -1 if the signal doesn't have a message
550# for signal without channel, nick or address, set to undef
551sub check_signal_message {
552        my ($signal, $parammessage, $server, $channelname, $nickname, $address, $condition, $extra) = @_;
553        my ($changed, $stopped, $context, $need_rebuild);
554        my $message = ($parammessage == -1) ? '' : $signal->[$parammessage];
555
556        return if (!$triggers_by_type{$condition});
557       
558        if ($recursion_depth > 10) {
559                Irssi::print("Trigger error: Maximum recursion depth reached, aborting trigger.", MSGLEVEL_CLIENTERROR);
560                return;
561        }
562        $recursion_depth++;
563
564TRIGGER:       
565        foreach my $trigger (@{$triggers_by_type{$condition}}) {
566                # check filters
567                foreach my $trigfilter (@{$trigger->{'filters'}}) {
568                        if (! ($trigfilter->[2]($trigfilter->[1], $signal,$parammessage,$server,$channelname,$nickname,$address,$condition,$extra))) {
569                       
570                                next TRIGGER;
571                        }
572                }
573               
574                # check regexp (and keep matches in @- and @+, so don't make a this a {block})
575                next if ($trigger->{'compregexp'} && ($parammessage == -1 || $message !~ m/$trigger->{'compregexp'}/));
576               
577                # if we got this far, it fully matched, and we need to do the replace/command/stop/once
578                my $expands = $extra;
579                $expands->{'M'} = $message,;
580                $expands->{'T'} = (defined($server)) ? $server->{'tag'} : '';
581                $expands->{'C'} = $channelname;
582                $expands->{'N'} = $nickname;
583                $expands->{'A'} = $address;
584                $expands->{'I'} = ((!defined($address)) ? '' : substr($address,0,index($address,'@')));
585                $expands->{'H'} = ((!defined($address)) ? '' : substr($address,index($address,'@')+1));
586                $expands->{'$'} = '$';
587                $expands->{';'} = ';';
588
589                if (defined($trigger->{'replace'})) { # it's a -replace
590                        $message =~ s/$trigger->{'compregexp'}/do_expands($trigger->{'compreplace'},$expands,$message)/ge;
591                        $changed = 1;
592                }
593               
594                if ($trigger->{'command'}) { # it's a (nonempty) -command
595                        my $command = $trigger->{'command'};
596                        # $1 = the stuff behind the $ we want to expand: a number, or a character from %expands
597                        $command = do_expands($command, $expands, $message);
598                       
599                        if (defined($server)) {
600                                if (defined($channelname) && $server->channel_find($channelname)) {
601                                        $context = $server->channel_find($channelname);
602                                } else {
603                                        $context = $server;
604                                }
605                        } else {
606                                $context = undef;
607                        }
608                       
609                        if (defined($context)) {
610                                $context->command("eval $command");
611                        } else {
612                                Irssi::command("eval $command");
613                        }
614                }
615
616                if ($trigger->{'debug'}) {
617                        print("DEBUG: trigger $condition pmesg=$parammessage message=$message server=$server->{tag} channel=$channelname nick=$nickname address=$address " . join(' ',map {$_ . '=' . $extra->{$_}} keys(%$extra)));
618                }
619               
620                if ($trigger->{'stop'}) {
621                        $stopped = 1;
622                }
623               
624                if ($trigger->{'once'}) {
625                        # find this trigger in the real trigger list, and remove it
626                        for (my $realindex=0; $realindex < scalar(@triggers); $realindex++) {
627                                if ($triggers[$realindex] == $trigger) {
628                                        splice (@triggers,$realindex,1);
629                                        last;
630                                }
631                        }
632                        $need_rebuild = 1;
633                }
634        }
635
636        if ($need_rebuild) {
637                rebuild();
638                $changed_since_last_save = 1;
639        }
640        if ($stopped) { # stopped with -stop
641                signal_stop();
642        } elsif ($changed) { # changed with -replace
643                $signal->[$parammessage] = $message;
644                signal_continue(@$signal);
645        }
646        $recursion_depth--;
647}
648
649# used in check_signal_message to expand $'s
650# $inthis is a string that can contain $ stuff (like 'foo$1bar$N')
651sub do_expands {
652        my ($inthis, $expands, $from) = @_;
653        # @+ and @- are copied because there are two s/// nested, and the inner needs the $1 and $2,... of the outer one
654        my @plus = @+;
655        my @min = @-;
656        my $p = \@plus; my $m = \@min;
657        $inthis =~ s/\$(\\*(\d+|[^0-9x{]|x[0-9a-fA-F][0-9a-fA-F]|{.*?}))/expand_and_escape($1,$expands,$m,$p,$from)/ge; 
658        return $inthis;
659}
660
661# \ $ and ; need extra escaping because we use eval
662sub expand_and_escape {
663        my $retval = expand(@_);
664        $retval =~ s/([\\\$;])/\\\1/g;
665        return $retval;
666}
667
668# used in do_expands (via expand_and_escape), to_expand is the part after the $
669sub expand {
670        my ($to_expand, $expands, $min, $plus, $from) = @_;
671        if ($to_expand =~ /^\d+$/) { # a number => look up in $vars
672                # from man perlvar:
673                # $3 is the same as "substr $var, $-[3], $+[3] - $-[3])"
674                return ($to_expand > @{$min} ? '' : substr($from,$min->[$to_expand],$plus->[$to_expand]-$min->[$to_expand]));
675        } elsif ($to_expand =~ s/^\\//) { # begins with \, so strip that from to_expand
676                my $exp = expand($to_expand,$expands,$min,$plus,$from); # first expand without \
677                $exp =~ s/([^a-zA-Z0-9])/\\\1/g; # escape non-word chars
678                return $exp;
679        } elsif ($to_expand =~ /^x([0-9a-fA-F]{2})/) { # $xAA
680                return chr(hex($1));
681        } elsif ($to_expand =~ /^{(.*?)}$/) { # ${foo}
682                return expand($1, $expands, $min, $plus, $from);
683        } else { # look up in $expands
684                return $expands->{$to_expand};
685        }
686}
687
688sub check_modes {
689        my ($has_modes, $need_modes) = @_;
690        my $matches;
691        my $switch = 1; # if a '-' if found, will be 0 (meaning the modes should not be set)
692        foreach my $need_mode (split /&/, $need_modes) {
693                $matches = 0;
694                foreach my $char (split //, $need_mode) {
695                        if ($char eq '-') {
696                                $switch = 0;
697                        } elsif ($char eq '+') {
698                                $switch = 1;
699                        } elsif ((index($has_modes, $char) != -1) == $switch) {
700                                $matches = 1;
701                                last;
702                        }
703                }
704                if (!$matches) {
705                        return 0;
706                }
707        }
708        return 1;
709}
710
711# get someones flags from people.pl or friends(_shasta).pl
712sub get_flags {
713        my ($chatnet, $channel, $nick, $address) = @_;
714        my $flags;
715        no strict 'refs';
716        if (defined %{ 'Irssi::Script::people::' }) {
717                if (defined ($channel)) {
718                        $flags = (&{ 'Irssi::Script::people::find_local_flags' }($chatnet,$channel,$nick,$address));
719                } else {
720                        $flags = (&{ 'Irssi::Script::people::find_global_flags' }($chatnet,$nick,$address));
721                }
722                $flags = join('',keys(%{$flags}));
723        } else {
724                my $shasta;
725                if (defined %{ 'Irssi::Script::friends_shasta::' }) {
726                        $shasta = 'friends_shasta';
727                } elsif (defined &{ 'Irssi::Script::friends::get_idx' }) {
728                        $shasta = 'friends';
729                } else {
730                        return undef;
731                }
732                my $idx = (&{ 'Irssi::Script::'.$shasta.'::get_idx' }($nick, $address));
733                if ($idx == -1) {
734                        return '';
735                }
736                $flags = (&{ 'Irssi::Script::'.$shasta.'::get_friends_flags' }($idx,undef));
737                if ($channel) {
738                        $flags .= (&{ 'Irssi::Script::'.$shasta.'::get_friends_flags' }($idx,$channel));
739                }
740        }
741        return $flags;
742}
743
744########################################################
745### internal stuff called by manage, needed by above ###
746########################################################
747
748my %mask_to_regexp = ();
749foreach my $i (0..255) {
750    my $ch = chr $i;
751    $mask_to_regexp{$ch} = "\Q$ch\E";
752}
753$mask_to_regexp{'?'} = '(.)';
754$mask_to_regexp{'*'} = '(.*)';
755
756sub compile_trigger {
757        my ($trigger) = @_;
758        my $regexp;
759       
760        if ($trigger->{'regexp'}) {
761                $regexp = $trigger->{'regexp'};
762        } elsif ($trigger->{'pattern'}) {
763                $regexp = $trigger->{'pattern'};
764                $regexp =~ s/(.)/$mask_to_regexp{$1}/g;
765        } else {
766                delete $trigger->{'compregexp'};
767                return;
768        }
769       
770        if ($trigger->{'nocase'}) {
771                $regexp = '(?i)' . $regexp;
772        }
773       
774        $trigger->{'compregexp'} = qr/$regexp/;
775       
776        if(defined($trigger->{'replace'})) {
777                (my $replace = $trigger->{'replace'}) =~ s/\$/\$\$/g;
778                $trigger->{'compreplace'} = Irssi::parse_special($replace);
779        }
780}
781
782# rebuilds triggers_by_type and updates signal binds
783sub rebuild {
784        %triggers_by_type = ();
785        foreach my $trigger (@triggers) {
786                if (!$trigger->{'disabled'}) {
787                        if ($trigger->{'all'}) {
788                                # -all is an alias for all types in @all_types for which the filters can apply
789ALLTYPES:
790                                foreach my $type (@all_types) {
791                                        # check if all filters can apply to $type
792                                        foreach my $filter (@{$trigger->{'filters'}}) {
793                                                if (! grep {$_ eq $type} $filters{$filter->[0]}->{'types'}) {
794                                                        next ALLTYPES;
795                                                }
796                                        }
797                                        push @{$triggers_by_type{$type}}, ($trigger);
798                                }
799                        }
800                       
801                        foreach my $type ($trigger->{'all'} ? @notall_types : @trigger_types) {
802                                if ($trigger->{$type}) {
803                                        push @{$triggers_by_type{$type}}, ($trigger);
804                                }
805                        }
806                }
807        }
808       
809        foreach my $signal (@signals) {
810                my $should_bind = 0;
811                foreach my $type (@{$signal->{'types'}}) {
812                        if (defined($triggers_by_type{$type})) {
813                                $should_bind = 1;
814                        }
815                }
816                if ($should_bind && !$signal->{'bind'}) {
817                        signal_add_first($signal->{'signal'}, $signal->{'sub'});
818                        $signal->{'bind'} = 1;
819                } elsif (!$should_bind && $signal->{'bind'}) {
820                        signal_remove($signal->{'signal'}, $signal->{'sub'});
821                        $signal->{'bind'} = 0;
822                }
823        }
824}
825
826################################
827### manage the triggers-list ###
828################################
829
830my $trigger_file; # cached setting
831
832sub sig_setup_changed {
833        $trigger_file = Irssi::settings_get_str('trigger_file');
834}
835
836sub autosave {
837        cmd_save() if ($changed_since_last_save);
838}
839
840# TRIGGER SAVE
841sub cmd_save {
842        my $io = new IO::File $trigger_file, "w";
843        if (defined $io) {
844                $io->print("#Triggers file version $VERSION\n");
845                foreach my $trigger (@triggers) {
846                        $io->print(to_string($trigger) . "\n");
847                }
848                $io->close;
849        }
850        Irssi::printformat(MSGLEVEL_CLIENTNOTICE, 'trigger_saved', $trigger_file);
851        $changed_since_last_save = 0;
852}
853
854# save on unload
855sub UNLOAD {
856        cmd_save();
857}
858
859# TRIGGER LOAD
860sub cmd_load {
861        sig_setup_changed(); # make sure we've read the trigger_file setting
862        my $converted = 0;
863        my $io = new IO::File $trigger_file, "r";
864        if (not defined $io) {
865                if (-e $trigger_file) {
866                        Irssi::print("Error opening triggers file", MSGLEVEL_CLIENTERROR);
867                }
868                return;
869        }
870        if (defined $io) {
871                @triggers = ();
872                my $text;
873                $text = $io->getline;
874                my $file_version = '';
875                if ($text =~ /^#Triggers file version (.*)\n/) {
876                        $file_version = $1;
877                }
878                if ($file_version lt '0.6.1+2') {
879                        no strict 'vars';
880                        $text .= $_ foreach ($io->getlines);
881                        my $rep = eval "$text";
882                        if (! ref $rep) {
883                                Irssi::print("Error in triggers file");
884                                return;
885                        }
886                        my @old_triggers = @$rep;
887               
888                        for (my $index=0;$index < scalar(@old_triggers);$index++) { 
889                                my $trigger = $old_triggers[$index];
890       
891                                if ($file_version lt '0.6.1') {
892                                        # convert old names: notices => pubnotices, actions => pubactions
893                                        foreach $oldname ('notices','actions') {
894                                                if ($trigger->{$oldname}) {
895                                                        delete $trigger->{$oldname};
896                                                        $trigger->{'pub'.$oldname} = 1;
897                                                        $converted = 1;
898                                                }
899                                        }
900                                }
901                                if ($file_version lt '0.6.1+1' && $trigger->{'modifiers'}) {
902                                        if ($trigger->{'modifiers'} =~ /i/) {
903                                                $trigger->{'nocase'} = 1;
904                                                Irssi::print("Trigger: trigger ".($index+1)." had 'i' in it's modifiers, it has been converted to -nocase");
905                                        }
906                                        if ($trigger->{'modifiers'} !~ /^[ig]*$/) {
907                                                Irssi::print("Trigger: trigger ".($index+1)." had unrecognised modifier '". $trigger->{'modifiers'} ."', which couldn't be converted.");
908                                        }
909                                        delete $trigger->{'modifiers'};
910                                        $converted = 1;
911                                }
912                               
913                                if (defined($trigger->{'replace'}) && ! $trigger->{'regexp'}) {
914                                        Irssi::print("Trigger: trigger ".($index+1)." had -replace but no -regexp, removed it");
915                                        splice (@old_triggers,$index,1);
916                                        $index--; # nr of next trigger now is the same as this one was
917                                }
918                               
919                                # convert to text with compat, and then to new trigger hash
920                                $text = to_string($trigger,1);
921                                my @args = &shellwords($text . ' a');
922                                my $trigger = parse_options({},@args);
923                                if ($trigger) {
924                                        push @triggers, $trigger;
925                                }
926                        }
927                } else { # new format
928                        while ( $text = $io->getline ) {
929                                chop($text);
930                                my @args = &shellwords($text . ' a');
931                                my $trigger = parse_options({},@args);
932                                if ($trigger) {
933                                        push @triggers, $trigger;
934                                }
935                        }
936                }
937        }
938        Irssi::printformat(MSGLEVEL_CLIENTNOTICE, 'trigger_loaded', $trigger_file);
939        if ($converted) {
940                Irssi::print("Trigger: Triggers file will be in new format next time it's saved.");
941        }
942        rebuild();
943}
944
945# escape for printing with to_string
946# <<abcdef>>      => << 'abcdef' >>
947# <<abc'def>>     => << "abc'def" >>
948# <<abc'def\x02>> => << 'abc'\''def\x02' >>
949sub param_to_string {
950        my ($text) = @_;
951        # avoid ugly escaping if we can use "-quotes without other escaping (no " or \)
952        if ($text =~ /^[^"\\]*'[^"\\]$/) {
953                return ' "' . $text . '" ';
954        }
955        # "'" signs without a (odd number of) \ in front of them, need be to escaped as '\''
956        # this is ugly :(
957        $text =~ s/(^|[^\\](\\\\)*)'/$1'\\''/g;
958        return " '$text' ";
959}
960
961# converts a trigger back to "-switch -options 'foo'" form
962# if $compat, $trigger is in the old format (used to convert)
963sub to_string {
964        my ($trigger, $compat) = @_;
965        my $string;
966       
967        foreach my $switch (@trigger_switches) {
968                if ($trigger->{$switch}) {
969                        $string .= '-'.$switch.' ';
970                }
971        }
972       
973        if ($compat) {
974                foreach my $filter (keys(%filters)) {
975                        if ($trigger->{$filter}) {
976                                $string .= '-' . $filter . param_to_string($trigger->{$filter});
977                        }
978                }
979        } else {
980                foreach my $trigfilter (@{$trigger->{'filters'}}) {
981                        $string .= '-' . $trigfilter->[0] . param_to_string($trigfilter->[1]);
982                }
983        }
984
985        foreach my $param (@trigger_params) {
986                if ($trigger->{$param} || ($param eq 'replace' && defined($trigger->{'replace'}))) {
987                        $string .= '-' . $param . param_to_string($trigger->{$param});
988                }
989        }
990        return $string;
991}
992
993# find a trigger (for REPLACE and DELETE), returns index of trigger, or -1 if not found
994sub find_trigger {
995        my ($data) = @_;
996        if ($data =~ /^[0-9]*$/ and defined($triggers[$data-1])) {
997                return $data-1;
998        } else {
999                for (my $i=0; $i < scalar(@triggers); $i++) {
1000                        if ($triggers[$i]->{'name'} eq $data) {
1001                                return $i;
1002                        }
1003                }
1004        }
1005        Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trigger_not_found', $data);
1006        return -1; # not found
1007}
1008
1009
1010# TRIGGER ADD <options>
1011sub cmd_add {
1012        my ($data, $server, $item) = @_;
1013        my @args = shellwords($data . ' a');
1014       
1015        my $trigger = parse_options({}, @args);
1016        if ($trigger) {
1017                push @triggers, $trigger;
1018                Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trigger_added', scalar(@triggers), to_string($trigger));
1019                rebuild();
1020                $changed_since_last_save = 1;
1021        }
1022}
1023
1024# TRIGGER CHANGE <nr> <options>
1025sub cmd_change {
1026        my ($data, $server, $item) = @_;
1027        my @args = shellwords($data . ' a');
1028        my $index = find_trigger(shift @args);
1029        if ($index != -1) {
1030                if(parse_options($triggers[$index], @args)) {
1031                        Irssi::print("Trigger " . ($index+1) ." changed to: ". to_string($triggers[$index]));
1032                }
1033                rebuild();
1034                $changed_since_last_save = 1;
1035        }
1036}
1037
1038# parses options for TRIGGER ADD and TRIGGER CHANGE
1039# if invalid args returns undef, else changes $thetrigger and returns it
1040sub parse_options {
1041        my ($thetrigger,@args) = @_;
1042        my ($trigger, $option);
1043       
1044        if (pop(@args) ne 'a') {
1045                Irssi::print("Syntax error, probably missing a closing quote", MSGLEVEL_CLIENTERROR);
1046                return undef;
1047        }
1048       
1049        %$trigger = %$thetrigger; # make a copy to prevent changing the given trigger if args doesn't parse
1050ARGS:   for (my $arg = shift @args; $arg; $arg = shift @args) {
1051                # expand abbreviated options, put in $option
1052                $arg =~ s/^-//;
1053                $option = undef;
1054                foreach my $ioption (@trigger_options) {
1055                        if (index($ioption, $arg) == 0) { # -$opt starts with $arg
1056                                if ($option) { # another already matched
1057                                        Irssi::print("Ambiguous option: $arg", MSGLEVEL_CLIENTERROR);
1058                                        return undef;
1059                                }
1060                                $option = $ioption;
1061                                last if ($arg eq $ioption); # exact match is unambiguous
1062                        }
1063                }
1064                if (!$option) {
1065                        Irssi::print("Unknown option: $arg", MSGLEVEL_CLIENTERROR);
1066                        return undef;
1067                }
1068
1069                # -<param> <value> or -no<param>
1070                foreach my $param (@trigger_params) {
1071                        if ($option eq $param) {
1072                                $trigger->{$param} = shift @args;
1073                                next ARGS;
1074                        }
1075                        if ($option eq 'no'.$param) {
1076                                $trigger->{$param} = undef;
1077                                next ARGS;
1078                        }
1079                }
1080
1081                # -[no]<switch>
1082                foreach my $switch (@trigger_switches) {
1083                        # -<switch>
1084                        if ($option eq $switch) {
1085                                $trigger->{$switch} = 1;
1086                                next ARGS;
1087                        }
1088                        # -no<switch>
1089                        elsif ($option eq 'no'.$switch) {
1090                                $trigger->{$switch} = undef;
1091                                next ARGS;
1092                        }
1093                }
1094               
1095                # -<filter> <value>
1096                if ($filters{$option}) {
1097                        push @{$trigger->{'filters'}}, [$option, shift @args, $filters{$option}->{'sub'}];
1098                        next ARGS;
1099                }
1100               
1101                # -<nofilter>
1102                if ($option =~ /^no(.*)$/ && $filters{$1}) {
1103                        my $filter = $1;
1104                        # the new filters are the old grepped for everything except ones with name $filter
1105                        @{$trigger->{'filters'}} = grep( $_->[0] ne $filter, @{$trigger->{'filters'}} );
1106                }
1107        }
1108       
1109        if (defined($trigger->{'replace'}) && ! $trigger->{'regexp'} && !$trigger->{'pattern'}) {
1110                Irssi::print("Trigger error: Can't have -replace without -regexp", MSGLEVEL_CLIENTERROR);
1111                return undef;
1112        }
1113
1114        if ($trigger->{'pattern'} && $trigger->{'regexp'}) {
1115                Irssi::print("Trigger error: Can't have -pattern and -regexp in same trigger", MSGLEVEL_CLIENTERROR);
1116                return undef;
1117        }
1118       
1119        # remove types that are implied by -all
1120        if ($trigger->{'all'}) {
1121                foreach my $type (@all_types) {
1122                        delete $trigger->{$type};
1123                }
1124        }
1125       
1126        # remove types for which the filters don't apply
1127        foreach my $type (@trigger_types) {
1128                if ($trigger->{$type}) {
1129                        foreach my $filter (@{$trigger->{'filters'}}) {
1130                                if (!grep {$_ eq $type} @{$filters{$filter->[0]}->{'types'}}) {
1131                                        Irssi::print("Warning: the filter -" . $filter->[0] . " can't apply to an event of type -$type, so I'm removing that type from this trigger.");
1132                                        delete $trigger->{$type};
1133                                }
1134                        }
1135                }
1136        }
1137
1138        # check if it has at least one type
1139        my $has_a_type;
1140        foreach my $type (@trigger_types) {
1141                if ($trigger->{$type}) {
1142                        $has_a_type = 1;
1143                        last;
1144                }
1145        }
1146        if (!$has_a_type && !$trigger->{'all'}) {
1147                Irssi::print("Warning: this trigger doesn't trigger on any type of message. you probably want to add -publics or -all");
1148        }
1149       
1150        compile_trigger($trigger);
1151        %$thetrigger = %$trigger; # copy changes to real trigger
1152        return $thetrigger;
1153}
1154
1155# TRIGGER DELETE <num>
1156sub cmd_del {
1157        my ($data, $server, $item) = @_;
1158        my @args = shellwords($data);
1159        my $index = find_trigger(shift @args);
1160        if ($index != -1) {
1161                Irssi::print("Deleted ". ($index+1) .": ". to_string($triggers[$index]));
1162                splice (@triggers,$index,1);
1163                rebuild();
1164                $changed_since_last_save = 1;
1165        }
1166}
1167
1168# TRIGGER MOVE <num> <num>
1169sub cmd_move {
1170        my ($data, $server, $item) = @_;
1171        my @args = &shellwords($data);
1172        my $index = find_trigger(shift @args);
1173        if ($index != -1) {
1174                my $newindex = shift @args;
1175                if ($newindex < 1 || $newindex > scalar(@triggers)) {
1176                        Irssi::print("$newindex is not a valid trigger number");
1177                        return;
1178                }
1179                Irssi::print("Moved from ". ($index+1) ." to $newindex: ". to_string($triggers[$index]));
1180                $newindex -= 1; # array starts counting from 0
1181                my $trigger = splice (@triggers,$index,1); # remove from old place
1182                splice (@triggers,$newindex,0,($trigger)); # insert at new place
1183                rebuild();
1184                $changed_since_last_save = 1;
1185        }
1186}
1187
1188# TRIGGER LIST
1189sub cmd_list {
1190        Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trigger_header');
1191        my $i=1;
1192        foreach my $trigger (@triggers) {
1193                Irssi::printformat(MSGLEVEL_CLIENTCRAP, 'trigger_line', $i++, to_string($trigger));
1194        }
1195}
1196
1197######################
1198### initialisation ###
1199######################
1200
1201command_bind('trigger help',\&cmd_help);
1202command_bind('help trigger',\&cmd_help);
1203command_bind('trigger add',\&cmd_add);
1204command_bind('trigger change',\&cmd_change);
1205command_bind('trigger move',\&cmd_move);
1206command_bind('trigger list',\&cmd_list);
1207command_bind('trigger delete',\&cmd_del);
1208command_bind('trigger save',\&cmd_save);
1209command_bind('trigger reload',\&cmd_load);
1210command_bind 'trigger' => sub {
1211    my ( $data, $server, $item ) = @_;
1212    $data =~ s/\s+$//g;
1213    command_runsub('trigger', $data, $server, $item);
1214};
1215
1216Irssi::signal_add('setup saved', \&autosave);
1217Irssi::signal_add('setup changed', \&sig_setup_changed);
1218
1219# This makes tab completion work
1220Irssi::command_set_options('trigger add',join(' ',@trigger_add_options));
1221Irssi::command_set_options('trigger change',join(' ',@trigger_options));
1222
1223Irssi::settings_add_str($IRSSI{'name'}, 'trigger_file', Irssi::get_irssi_dir()."/triggers");
1224
1225cmd_load();
Note: See TracBrowser for help on using the repository browser.