1use strict;
2use Irssi;
3use POSIX;
4use Socket;
5use vars qw($VERSION %IRSSI);
6
7$VERSION = "3.7";
8%IRSSI = (
9	authors     => 'Toni Salom�ki',
10	name        => 'autoopper',
11	contact     => 'Toni@IRCNet',
12	description => 'Auto-op script with dynamic address support and random delay',
13	license     => 'GNU GPLv2 or later',
14	url         => 'http://vinku.dyndns.org/irssi_scripts/'
15);
16
17# This is a script to auto-op people on a certain channel (all or, represented with *).
18# Users are auto-opped on join with random delay.
19# There is a possibility to use dns aliases (for example dyndns.org) for getting the correct address.
20# The auto-op list is stored into ~/.irssi/autoop
21#
22# To get the dynamic addresses to be refreshed automatically, set value to autoop_dynamic_refresh (in hours)
23# The value will be used next time the script is loaded (at startup or manual load)
24#
25# NOTICE: the datafile is in completely different format than in 1.0 and this version cannot read it. Sorry.
26#
27
28# COMMANDS:
29#
30# autoop_show - Displays list of auto-opped hostmasks & channels
31#               The current address of dynamic host is displayed in parenthesis
32#
33# autoop_add - Add new auto-op. Parameters hostmask, channel (or *) and dynamic flag
34#
35#    Dynamic flag has 3 different values:
36#      0: treat host as a static ip
37#      1: treat host as an alias for dynamic ip
38#      2: treat host as an alias for dynamic ip, but do not resolve the ip (not normally needed)
39#
40# autoop_del - Remove auto-op
41#
42# autoop_save - Save auto-ops to file (done normally automatically)
43#
44# autoop_load - Load auto-ops from file (use this if you have edited the autoop -file manually)
45#
46# autoop_check - Check all channels and op people needed
47#
48# autoop_dynamic - Refresh dynamic addresses (automatically if parameter set)
49#
50# Data is stored in ~/.irssi/autoop
51# format: host	channels	flag
52# channels separated with comma
53# one host per line
54
55my (%oplist);
56my (@opitems);
57srand();
58
59#resolve dynamic host
60sub resolve_host {
61	my ($host, $dyntype) = @_;
62
63	if (my $iaddr = inet_aton($host)) {
64		if ($dyntype ne "2") {
65			if (my $newhost = gethostbyaddr($iaddr, AF_INET)) {
66				return $newhost;
67			} else {
68				return inet_ntoa($iaddr);
69			}
70		} else {
71			return inet_ntoa($iaddr);
72		}
73	}
74	return "error";
75}
76
77# return list of dynamic hosts with real addresses
78sub fetch_dynamic_hosts {
79	my %hostcache;
80	my $resultext;
81	foreach my $item (@opitems) {
82		next if ($item->{dynamic} ne "1" && $item->{dynamic} ne "2");
83
84		my (undef, $host) = split(/\@/, $item->{mask}, 2);
85
86		# fetch the host's real address (if not cached)
87		unless ($hostcache{$host}) {
88			$hostcache{$host} = resolve_host($host, $item->{dynamic});
89			$resultext .= $host . "\t" . $hostcache{$host} . "\n";
90		}
91	}
92	chomp $resultext;
93	return $resultext;
94}
95
96# fetch real addresses for dynamic hosts
97sub cmd_change_dynamic_hosts {
98	pipe READ, WRITE;
99	my $pid = fork();
100
101	unless (defined($pid)) {
102		Irssi::print("Can't fork - aborting");
103		return;
104	}
105
106	if ($pid > 0) {
107		# the original process, just add a listener for pipe
108		close (WRITE);
109		Irssi::pidwait_add($pid);
110		my $target = {fh => \*READ, tag => undef};
111		$target->{tag} = Irssi::input_add(fileno(READ), INPUT_READ, \&read_dynamic_hosts, $target);
112	} else {
113		# the new process, fetch addresses and write to the pipe
114		print WRITE fetch_dynamic_hosts;
115		close (READ);
116		close (WRITE);
117		POSIX::_exit(1);
118	}
119}
120
121# get dynamic hosts from pipe and change them to users
122sub read_dynamic_hosts {
123 	my $target = shift;
124	my $rh = $target->{fh};
125	my %hostcache;
126
127	while (<$rh>) {
128		chomp;
129		my ($dynhost, $realhost, undef) = split (/\t/, $_, 3);
130		$hostcache{$dynhost} = $realhost;
131	}
132
133	close($target->{fh});
134	Irssi::input_remove($target->{tag});
135
136	my $mask;
137	my $count = 0;
138	undef %oplist if (%oplist);
139
140	foreach my $item (@opitems) {
141		if ($item->{dynamic} eq "1" || $item->{dynamic} eq "2") {
142			my ($user, $host) = split(/\@/, $item->{mask}, 2);
143
144			$count++ if ($item->{dynmask} ne $hostcache{$host});
145			$item->{dynmask} = $hostcache{$host};
146			$mask = $user . "\@" . $hostcache{$host};
147		} else {
148			$mask = $item->{mask};
149		}
150
151		foreach my $channel (split (/,/,$item->{chan})) {
152			$oplist{$channel} .= "$mask ";
153		}
154	}
155	chop %oplist;
156	Irssi::print("$count dynamic hosts changed") if ($count > 0);
157}
158
159# Save data to file
160sub cmd_save_autoop {
161	my $file = Irssi::get_irssi_dir."/autoop";
162	open FILE, ">", "$file" or return;
163
164	foreach my $item (@opitems) {
165		printf FILE ("%s\t%s\t%s\n", $item->{mask}, $item->{chan}, $item->{dynamic});
166	}
167
168	close FILE;
169	Irssi::print("Auto-op list saved to $file");
170}
171
172# Load data from file
173sub cmd_load_autoop {
174	my $file = Irssi::get_irssi_dir."/autoop";
175	open FILE, "<","$file" or return;
176	undef @opitems if (@opitems);
177
178	while (<FILE>) {
179		chomp;
180		my ($mask, $chan, $dynamic, undef) = split (/\t/, $_, 4);
181		my $item = {mask=>$mask, chan=>$chan, dynamic=>$dynamic, dynmask=>undef};
182		push (@opitems, $item);
183	}
184
185	close FILE;
186	Irssi::print("Auto-op list reloaded from $file");
187	cmd_change_dynamic_hosts;
188}
189
190# Show who's being auto-opped
191sub cmd_show_autoop {
192	my %list;
193	foreach my $item (@opitems) {
194		foreach my $channel (split (/,/,$item->{chan})) {
195			$list{$channel} .= "\n" . $item->{mask};
196			$list{$channel} .= " (" . $item->{dynmask} . ")" if ($item->{dynmask});
197		}
198	}
199
200	Irssi::print("All channels:" . $list{"*"}) if (exists $list{"*"});
201	delete $list{"*"}; #this is already printed, so remove it
202	foreach my $channel (sort (keys %list)) {
203		Irssi::print("$channel:" . $list{$channel});
204	}
205}
206
207# Add new auto-op
208sub cmd_add_autoop {
209	my ($data) = @_;
210	my ($mask, $chan, $dynamic, undef) = split(" ", $data, 4);
211	my $found = 0;
212
213	if ($chan eq "" || $mask eq "" || !($mask =~ /.+!.+@.+/)) {
214		Irssi::print("Invalid hostmask. It must contain both ! and @.") if (!($mask =~ /.+!.+@.+/));
215		Irssi::print("Usage: /autoop_add <hostmask> <*|#channel> [dynflag]");
216		Irssi::print("Dynflag: 0 normal, 1 dynamic, 2 dynamic without resolving");
217		return;
218	}
219
220	foreach my $item (@opitems) {
221		next unless ($item->{mask} eq $mask);
222		$found = 1;
223		$item->{chan} .= ",$chan";
224		last;
225	}
226
227	if ($found == 0) {
228		$dynamic = "0" unless ($dynamic eq "1" || $dynamic eq "2");
229		my $item = {mask=>$mask, chan=>$chan, dynamic=>$dynamic, dynmask=>undef};
230		push (@opitems, $item);
231	}
232
233	$oplist{$chan} .= " $mask";
234
235	Irssi::print("Added auto-op: $chan: $mask");
236}
237
238# Remove autoop
239sub cmd_del_autoop {
240	my ($data) = @_;
241	my ($mask, $channel, undef) = split(" ", $data, 3);
242
243	if ($channel eq "" || $mask eq "") {
244		Irssi::print("Usage: /autoop_del <hostmask> <*|#channel>");
245		return;
246	}
247
248	my $i=0;
249	foreach my $item (@opitems) {
250		if ($item->{mask} eq $mask) {
251			if ($channel eq "*" || $item->{chan} eq $channel) {
252				splice @opitems, $i, 1;
253				Irssi::print("Removed: $mask");
254			} else {
255				my $newchan;
256				foreach my $currchan (split (/,/,$item->{chan})) {
257					if ($channel eq $currchan) {
258						Irssi::print("Removed: $channel from $mask");
259					} else {
260						$newchan .= $currchan . ",";
261					}
262				}
263				chop $newchan;
264				Irssi::print("Couldn't remove $channel from $mask") if ($item->{chan} eq $newchan);
265				$item->{chan} = $newchan;
266			}
267			last;
268		}
269		$i++;
270	}
271}
272
273# Do the actual opping
274sub do_autoop {
275	my $target = shift;
276
277	Irssi::timeout_remove($target->{tag});
278
279	# nick has to be fetched again, because $target->{nick}->{op} is not updated
280	my $nick = $target->{chan}->nick_find($target->{nick}->{nick});
281
282	# if nick is changed during delay, it will probably be lost here...
283	if ($nick->{nick} ne "") {
284		if ($nick->{host} eq $target->{nick}->{host}) {
285			$target->{chan}->command("op " . $nick->{nick}) unless ($nick->{op});
286		} else {
287			Irssi::print("Host changed for nick during delay: " . $nick->{nick});
288		}
289	}
290	undef $target;
291}
292
293# Someone joined, might be multiple person. Check if opping is needed
294sub event_massjoin {
295	my ($channel, $nicklist) = @_;
296	my @nicks = @{$nicklist};
297
298	return if (!$channel->{chanop});
299
300	my $masks = $oplist{"*"} . " " . $oplist{$channel->{name}};
301
302	foreach my $nick (@nicks) {
303		my $host = $nick->{host};
304		$host=~ s/^~//g; # remove this if you don't want to strip ~ from username (no ident)
305		next unless ($channel->{server}->masks_match($masks, $nick->{nick}, $host));
306
307		my $min_delay = Irssi::settings_get_int("autoop_min_delay");
308		my $max_delay = Irssi::settings_get_int("autoop_max_delay") - $min_delay;
309		my $delay = int(rand($max_delay)) + $min_delay;
310
311		my $target = {nick => $nick, chan => $channel, tag => undef};
312
313		$target->{tag} = Irssi::timeout_add($delay, 'do_autoop', $target);
314	}
315
316}
317
318# Check channel op status
319sub do_channel_check {
320	my $target = shift;
321
322	Irssi::timeout_remove($target->{tag});
323
324	my $channel = $target->{chan};
325	my $server = $channel->{server};
326	my $masks = $oplist{"*"} . " " . $oplist{$channel->{name}};
327	my $nicks = "";
328
329	foreach my $nick ($channel->nicks()) {
330		next if ($nick->{op});
331
332		my $host = $nick->{host};
333		$host=~ s/^~//g; # remove this if you don't want to strip ~ from username (no ident)
334
335		if ($server->masks_match($masks, $nick->{nick}, $host)) {
336			$nicks = $nicks . " " . $nick->{nick};
337		}
338	}
339	$channel->command("op" . $nicks) unless ($nicks eq "");
340
341	undef $target;
342}
343
344#check people needing opping after getting ops
345sub event_nickmodechange {
346	my ($channel, $nick, $setby, $mode, $type) = @_;
347
348	return unless (($mode eq '@') && ($type eq '+'));
349
350	my $server = $channel->{server};
351
352	return unless ($server->{nick} eq $nick->{nick});
353
354	my $min_delay = Irssi::settings_get_int("autoop_min_delay");
355	my $max_delay = Irssi::settings_get_int("autoop_max_delay") - $min_delay;
356	my $delay = int(rand($max_delay)) + $min_delay;
357
358	my $target = {chan => $channel, tag => undef};
359
360	$target->{tag} = Irssi::timeout_add($delay, 'do_channel_check', $target);
361}
362
363#Check all channels / all users if someone needs to be opped
364sub cmd_autoop_check {
365	my ($data, $server, $witem) = @_;
366
367	foreach my $channel ($server->channels()) {
368		Irssi::print("Checking: " . $channel->{name});
369		next if (!$channel->{chanop});
370
371		my $masks = $oplist{"*"} . " " . $oplist{$channel->{name}};
372
373		foreach my $nick ($channel->nicks()) {
374			next if ($nick->{op});
375
376			my $host = $nick->{host};
377			$host=~ s/^~//g; # remove this if you don't want to strip ~ from username (no ident)
378
379			if ($server->masks_match($masks, $nick->{nick}, $host)) {
380				$channel->command("op " . $nick->{nick}) if (!$nick->{op});
381			}
382		}
383	}
384}
385
386#Set dynamic refresh period.
387sub set_dynamic_refresh {
388	my $refresh = Irssi::settings_get_int("autoop_dynamic_refresh");
389	return if ($refresh == 0);
390
391	Irssi::print("Dynamic host refresh set for $refresh hours");
392	Irssi::timeout_add($refresh*3600000, 'cmd_change_dynamic_hosts', undef);
393}
394
395Irssi::command_bind('autoop_show', 'cmd_show_autoop');
396Irssi::command_bind('autoop_add', 'cmd_add_autoop');
397Irssi::command_bind('autoop_del', 'cmd_del_autoop');
398Irssi::command_bind('autoop_save', 'cmd_save_autoop');
399Irssi::command_bind('autoop_load', 'cmd_load_autoop');
400Irssi::command_bind('autoop_check', 'cmd_autoop_check');
401Irssi::command_bind('autoop_dynamic', 'cmd_change_dynamic_hosts');
402Irssi::signal_add_last('massjoin', 'event_massjoin');
403Irssi::signal_add_last('setup saved', 'cmd_save_autoop');
404Irssi::signal_add_last('setup reread', 'cmd_load_autoop');
405Irssi::signal_add_last("nick mode changed", "event_nickmodechange");
406Irssi::settings_add_int('autoop', 'autoop_max_delay', 15000);
407Irssi::settings_add_int('autoop', 'autoop_min_delay', 1000);
408Irssi::settings_add_int('autoop', 'autoop_dynamic_refresh', 0);
409
410
411cmd_load_autoop;
412set_dynamic_refresh;
413