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