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 | |
---|
20 | use strict; |
---|
21 | use Irssi 20020324 qw(command_bind command_runsub command signal_add_first signal_continue signal_stop signal_remove); |
---|
22 | use Text::ParseWords; |
---|
23 | use IO::File; |
---|
24 | use 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 | |
---|
37 | sub cmd_help { |
---|
38 | Irssi::print (<<'SCRIPTHELP_EOF', MSGLEVEL_CLIENTCRAP); |
---|
39 | |
---|
40 | TRIGGER LIST |
---|
41 | TRIGGER SAVE |
---|
42 | TRIGGER RELOAD |
---|
43 | TRIGGER MOVE <number> <number> |
---|
44 | TRIGGER DELETE <number> |
---|
45 | TRIGGER CHANGE <number> ... |
---|
46 | TRIGGER ADD ... |
---|
47 | |
---|
48 | When to match: |
---|
49 | On 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 | |
---|
67 | Filters (conditions) the event has to satisfy. They all take one parameter. |
---|
68 | If 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 | |
---|
84 | What 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 | |
---|
107 | Other 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 | |
---|
111 | Examples: |
---|
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 | |
---|
122 | Examples 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 ' ' |
---|
137 | SCRIPTHELP_EOF |
---|
138 | } # / |
---|
139 | |
---|
140 | my @triggers; # array of all triggers |
---|
141 | my %triggers_by_type; # hash mapping types on triggers of that type |
---|
142 | my $recursion_depth = 0; |
---|
143 | my $changed_since_last_save = 0; |
---|
144 | |
---|
145 | ############### |
---|
146 | ### formats ### |
---|
147 | ############### |
---|
148 | |
---|
149 | Irssi::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 |
---|
163 | my @allchanmsg_types = qw(publics pubactions pubnotices pubctcps pubctcpreplies parts quits kicks topics); |
---|
164 | # trigger types with a message |
---|
165 | my @allmsg_types = (@allchanmsg_types, qw(privmsgs privactions privnotices privctcps privctcpreplies dcc_msgs dcc_actions dcc_ctcps)); |
---|
166 | # trigger types with a channel |
---|
167 | my @allchan_types = (@allchanmsg_types, qw(mode_channel mode_nick joins invites)); |
---|
168 | # trigger types in -all |
---|
169 | my @all_types = (@allmsg_types, qw(mode_channel mode_nick joins invites nick_changes)); |
---|
170 | # trigger types with a server |
---|
171 | my @all_server_types = (@all_types, qw(rawin notify_join notify_part notify_away notify_unaway notify_unidle)); |
---|
172 | # all trigger types |
---|
173 | my @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; |
---|
176 | my @notall_types = qw(rawin notify_join notify_part notify_away notify_unaway notify_unidle send_command send_text beep); |
---|
177 | |
---|
178 | my @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 | |
---|
387 | sub 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 | |
---|
402 | my %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 | |
---|
514 | sub get_address { |
---|
515 | my ($nick, $server, $channel) = @_; |
---|
516 | my $nickrec = get_nickrec($nick, $server, $channel); |
---|
517 | return $nickrec ? $nickrec->{'host'} : undef; |
---|
518 | } |
---|
519 | sub 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 | |
---|
526 | sub 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 |
---|
539 | my @trigger_switches = (@trigger_types, qw(all nocase stop once debug disabled)); |
---|
540 | # parameters (with an argument) |
---|
541 | my @trigger_params = qw(pattern regexp command replace name); |
---|
542 | # list of all options (including switches) for /TRIGGER ADD |
---|
543 | my @trigger_add_options = (@trigger_switches, @trigger_params, keys(%filters)); |
---|
544 | # same for /TRIGGER CHANGE, this includes the -no<option>'s |
---|
545 | my @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 |
---|
551 | sub 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 | |
---|
564 | TRIGGER: |
---|
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') |
---|
651 | sub 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 |
---|
662 | sub 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 $ |
---|
669 | sub 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 | |
---|
688 | sub 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 |
---|
712 | sub 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 | |
---|
748 | my %mask_to_regexp = (); |
---|
749 | foreach 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 | |
---|
756 | sub 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 |
---|
783 | sub 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 |
---|
789 | ALLTYPES: |
---|
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 | |
---|
830 | my $trigger_file; # cached setting |
---|
831 | |
---|
832 | sub sig_setup_changed { |
---|
833 | $trigger_file = Irssi::settings_get_str('trigger_file'); |
---|
834 | } |
---|
835 | |
---|
836 | sub autosave { |
---|
837 | cmd_save() if ($changed_since_last_save); |
---|
838 | } |
---|
839 | |
---|
840 | # TRIGGER SAVE |
---|
841 | sub 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 |
---|
855 | sub UNLOAD { |
---|
856 | cmd_save(); |
---|
857 | } |
---|
858 | |
---|
859 | # TRIGGER LOAD |
---|
860 | sub 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' >> |
---|
949 | sub 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) |
---|
963 | sub 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 |
---|
994 | sub 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> |
---|
1011 | sub 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> |
---|
1025 | sub 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 |
---|
1040 | sub 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 |
---|
1050 | ARGS: 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> |
---|
1156 | sub 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> |
---|
1169 | sub 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 |
---|
1189 | sub 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 | |
---|
1201 | command_bind('trigger help',\&cmd_help); |
---|
1202 | command_bind('help trigger',\&cmd_help); |
---|
1203 | command_bind('trigger add',\&cmd_add); |
---|
1204 | command_bind('trigger change',\&cmd_change); |
---|
1205 | command_bind('trigger move',\&cmd_move); |
---|
1206 | command_bind('trigger list',\&cmd_list); |
---|
1207 | command_bind('trigger delete',\&cmd_del); |
---|
1208 | command_bind('trigger save',\&cmd_save); |
---|
1209 | command_bind('trigger reload',\&cmd_load); |
---|
1210 | command_bind 'trigger' => sub { |
---|
1211 | my ( $data, $server, $item ) = @_; |
---|
1212 | $data =~ s/\s+$//g; |
---|
1213 | command_runsub('trigger', $data, $server, $item); |
---|
1214 | }; |
---|
1215 | |
---|
1216 | Irssi::signal_add('setup saved', \&autosave); |
---|
1217 | Irssi::signal_add('setup changed', \&sig_setup_changed); |
---|
1218 | |
---|
1219 | # This makes tab completion work |
---|
1220 | Irssi::command_set_options('trigger add',join(' ',@trigger_add_options)); |
---|
1221 | Irssi::command_set_options('trigger change',join(' ',@trigger_options)); |
---|
1222 | |
---|
1223 | Irssi::settings_add_str($IRSSI{'name'}, 'trigger_file', Irssi::get_irssi_dir()."/triggers"); |
---|
1224 | |
---|
1225 | cmd_load(); |
---|