autovoice.pl

Code Index:


   1 #!/usr/bin/perl
   2 use strict;
   3 use warnings;
   4 
   5 BEGIN {
   6 	unless (exists $::{"Irssi::"}) {
   7 		require Pod::Usage;
   8 		Pod::Usage::pod2usage(-verbose => 2);
   9 	} 
  10 }
  11 
  12 use Irssi;
  13 our $VERSION = '0.05';
  14 our %IRSSI = (
  15 		authors => 'aluser',
  16 		name => 'autovoice',
  17 		description => 'autovoice',
  18 		license => 'GPL',
  19 	);
  20 
  21 our %bad;
  22 


SYNOPSIS

        /script load autovoice
        /autovoice add #somechannel
Idle on #somechannel as [half]op, and you will voice people :)


MOTIVATION

This is certainly not a new concept, but I dislike many implementations of autovoicing because they are not as intelligent as they could be. Blindly voicing everyone who joins your channel is dumb, because it removes the protection that +m is supposed to give you. A troublemake need merely to rejoin the channel to get his voice back. You probably want to voice newcomers to your channel, so a password or hostmask system is no good. Besides, it's intuitive that anybody leaving the channel without voice and quickly rejoining is trying to leverage your autovoicer! So, the main purpose of this script is to automatically detect these people and not voice them.

The other important feature is fine-grained control over where you voice people. You might want to autovoice in efnet #foo but not in dalnet #foo. The /autovoice add command gives you -server and -ircnet options to control on which channels you will autovoice, even if the channels have identical names.

I still consider this script to be lightly tested, but I do hope that it is well documented enough that it can be debugged well.


INSTALL

Just place this script in ~/.irssi/scripts. To have it load automatically when you start Irssi, do this:

        mkdir -p ~/.irssi/scripts/autorun
        ln -s ../autovoice.pl ~/.irssi/scripts/autorun/

If you haven't figured it out yet, you can run the script outside of Irssi to get a man page type document, like this:

        chmod +x autovoice.pl
        ./autovoice.pl


COMMANDS

/autovoice add

This is a helper to add channels to autovoice_channels for you. I'm going to explain this by example:

        /autovoice add #channelfoo
        /autovoice add -server irc.foo.com #barbarfoo
        /autovoice add -ircnet EFNet #perlhelp
        /autovoice add -server irc.efnet.org -ircnet Undernet #irssi

Note that the last example actually adds two ``channels'' to the setting, both named #irssi. The channel will be valid on Undernet or the server irc.efnet.org.

/autovoice remove

This is a helper to remove channels from autovoice_channels for you. Example:

        /autovoice remove #somechannel
        /autovoice remove #channel1 #channel2
/autovoice dump

Mostly for debugging, this dumps the perl hash containing blacklisted nicks to your screen.

/autovoice flush

Flush the blacklists.


SETTINGS

bool autovoice = ON

Set autovoicing on or off.

string autovoice_channels =

Control which channels we will autovoice. The simplest form is

        #channel1 , #channel2 , #channel3

Space before the commas is mandatory; after is optional. For any channel in the list, you may specify a chatnet or a server like this:

        #channel1 , #channel2 =>SOMECHATNET , #channel3 @some.server.com

Space after the channels and before the => or @ is required. Space after the => or @ is optional. (not shown)

See autovoice add and autovoice remove for wrappers to this.

int autovoice_cycletime = 600

Control the amount of time, in seconds, for which we remember that a nick left a channel without voice.

bool autovoice_voice_ops = OFF

Whether or not to give voice to people who already have op or halfop

bool autovoice_use_ident = OFF

Whether to distinguish between nicks which have the same host but different user names. (nick![ident@host] vs nick!ident@[host])

 120 
 121 


BUGS

Plenty.

&add will add duplicate channels

Error checking in &add is weak.

Setting autovoice_use_ident causes the existing blacklists to be ineffective.

parse_channels and deparse_channels mix up the ordering of the channels in the autovoice_channels setting. This is a property of the hash used to represent the setting.

remove doesn't let you remove only one channel when several use the same name.

Setting autovoice_cycletime does not change the timing for entries already in the badlist, only for entries made after the setting is changed. As far as I can tell, the alternatives are to A) Have a potentially ever-growing %bad, or B) to run a cleanup on a timer which must traverse all of %bad.

 155 
 156 


HACKING

This section is for people interested in tweaking/fixing/improving/developing/hacking this script. It describes every subroutine and data structure in the script. If you do not know Perl you should stop reading here.

Variables ending in _rec are Irssi objects of some sort. I also use _text to indicate normal strings.

DATA STRUCTURES

%bad

This hash holds the badlists for all channels. Each key is a tag as supplied by $server_rec-{tag}>. Each value is a hash reference as follows:

Each key is a lowercased channel name as given by lc $channel_rec->{name} . Each value is a hash referenc described as follows:

Each key is a lowercased host. If the host was marked bad while autovoice_use_ident was set, it is in the form ``username@host.com''. If not, it is just ``host.com''. Each value is a tag as returned by Irssi::timeout_add. This is used to remove the callback which is planning to remove the entry from the badlist after autovoice_cycletime expires.

%commands

This hash holds the commands invoked as /autovoice <command> [ arg1 arg2 ... ]. Each key is the lowercased name of a command, and each value is a reference to a subroutine. The subroutine should expect an Irssi Server object, a WindowItem object, and a list of user supplied arguments. The case of the arguments is left as supplied by the user.

 182 

SUBROUTINES

massjoin($channel_rec, $nicks_ray)

The nicks in the array referenced by $nicks_ray are joining $channel_rec. This is an irssi signal handler.

 192 
 193 sub massjoin {
 194 	my ($channel_rec, $nicks_ray) = @_;
 195 	voicem($channel_rec, @$nicks_ray);
 196 }
 197 
message_part($server_rec, $channel_text, $nick_text, $addr, $reason)

A nick is parting a channel. $addr and $reason are not used. This is an irssi signal handler.

 203 
 204 sub message_part {
 205 	my ($server_rec, $channel_text, $nick_text) = @_;
 206 	#Irssi::print("chan: $channel_text, nick: $nick_text");
 207 	#return unless defined $nick_text;	# happens if the part was us
 208 	no warnings;
 209 	my $channel_rec = $server_rec->channel_find($channel_text);
 210 	use warnings;
 211 	return unless defined $channel_rec;
 212 	my $nick_rec = $channel_rec->nick_find($nick_text);
 213 	partem($channel_rec, $nick_rec);
 214 }
 215 
message_quit($server_rec, $nick_text, $addr, $reason)

A nick is quiting the server. $addr and $reason are not used. This is an irssi signal handler.

 221 
 222 sub message_quit {
 223 	my ($server_rec, $nick_text, $addr, $reason) = @_;
 224 	my $chanstring = $server_rec->get_channels();
 225 	$chanstring =~ s/ .*//; #strip channel keys
 226 	my @channels_text = split /,/, $chanstring;
 227 	no warnings;
 228 	my @channels_rec =
 229 		map { $server_rec->channel_find($_) } @channels_text;
 230 	use warnings;
 231 	for (@channels_rec) {
 232 		my $nick_rec = $_->nick_find($nick_text);
 233 		if (defined $nick_rec) {
 234 			partem($_, $nick_rec);
 235 		}
 236 	}
 237 }
 238 
message_kick($server_rec, $channel_text, $nick_text, $addr, $reason)

Called when a nick is kicked from a channel. This is an Irssi signal handler.

 244 
 245 sub message_kick {
 246 	my ($server_rec, $channel_text, $nick_text) = @_;
 247 	my $channel_rec = $server_rec->channel_find($channel_text);
 248 	return unless defined $channel_rec;
 249 	my $nick_rec = $channel_rec->nick_find($nick_text);
 250 	partem($channel_rec, $nick_rec);
 251 }
 252 
voicem($channel_rec, @nicks)

This voices all of @nicks on $channel_rec, provided they aren't in the blacklist.

 258 
 259 sub voicem {
 260 	my ($channel_rec, @nicks) = @_;
 261 	if (is_auto($channel_rec)) {
 262 		for my $nick_rec (@nicks) {
 263 			unless (is_bad($channel_rec, $nick_rec)
 264 					or $nick_rec->{voice}) {
 265 				if (get_voiceops() or
 266 						!($nick_rec->{op} or $nick_rec->{halfop})) {
 267 					my $nick_text = $nick_rec->{nick};
 268 					$channel_rec->command("voice $nick_text");
 269 				}
 270 			}
 271 		}
 272 	}
 273 }
 274 
partem($channel_rec, $nick_rec)

Called when a nick is leaving a channel, by any means. This is what decides whether the nick does or does not have voice.

 280 
 281 sub partem {
 282 	my ($channel_rec, $nick_rec) = @_;
 283 	#$channel_rec->print("partem called");
 284 	if (is_auto($channel_rec)) {
 285 		#$channel_rec->print("this channel is autovoiced.");
 286 		if (not $nick_rec->{voice} and
 287 				not $nick_rec->{op} and
 288 				not $nick_rec->{halfop}) {
 289 			#$channel_rec->print("nick leaving with no voice");
 290 			make_bad($channel_rec, $nick_rec);
 291 		} else {
 292 			make_unbad($channel_rec, $nick_rec);
 293 		}
 294 	}
 295 }
 296 
is_bad($channel_rec, $nick_rec)

Returns 1 if $nick_rec is blacklisted on $channel_rec, 0 otherwise.

 302 
 303 sub is_bad {
 304 	my ($channel_rec, $nick_rec) = @_;
 305 	my $server_tag = $channel_rec->{server}->{tag};
 306 	my $channel_text = lc $channel_rec->{name};
 307 	my $host_text = lc $nick_rec->{host};
 308 	if (not get_useident()) {
 309 		$host_text =~ s/.*?\@//;
 310 	}
 311 	#$channel_rec->print("calling is_bad {$server_tag}{$channel_text}{$host_text}");
 312 	return
 313 		exists $bad{$server_tag} &&
 314 		exists $bad{$server_tag}{$channel_text} &&
 315 		exists $bad{$server_tag}{$channel_text}{$host_text};
 316 }
 317 
make_bad($channel_rec, $nick_rec)

Blacklist $nick_rec on $channel_rec for autovoice_cycletime seconds.

 323 
 324 sub make_bad {
 325 	my ($channel_rec, $nick_rec) = @_;
 326 	my $tag = $channel_rec->{server}->{tag};
 327 	my $channel_text = lc $channel_rec->{name};
 328 	my $host_text = lc $nick_rec->{host};
 329 	if (not get_useident()) {
 330 		$host_text =~ s/.*?\@//;
 331 	}
 332 	#$channel_rec->print("channel_rec: ".ref($channel_rec)."nick_rec: ".ref($nick_rec).". make bad $tag, $channel_text, $host_text");
 333 	Irssi::timeout_remove($bad{$tag}{$channel_text}{$host_text})
 334 			if exists $bad{$tag}{$channel_text}{$host_text};
 335 	$bad{$tag}{$channel_text}{$host_text} =
 336 			Irssi::timeout_add(get_cycletime(),
 337 							'timeout',
 338 							[ $channel_rec, $nick_rec ]);
 339 }
 340 
timeout([$channel_rec, $nick_rec])

This is the irssi timeout callback which removes $nick_rec from the blacklist for $channel_rec when autovoice_cycletime seconds have elapsed. make_unbad finds the tag in the badlist in order to keep this from being called again. Note that it only takes one argument, an array ref

 346 
 347 sub timeout {
 348 	my ($channel_rec, $nick_rec) = @{$_[0]};
 349 	#$channel_rec->print("timing out");
 350 	make_unbad($channel_rec, $nick_rec);
 351 }
 352 
make_unbad($channel_rec, $nick_rec)

Remove $nick_rec from the blacklist for $channel_rec

 358 
 359 sub make_unbad {
 360 	my ($channel_rec, $nick_rec) = @_;
 361 	my $tag = $channel_rec->{server}->{tag};
 362 	my $channel_text = lc $channel_rec->{name};
 363 	my $host_text = lc $nick_rec->{host};
 364 	if (not get_useident()) {
 365 		$host_text =~ s/.*\@//;
 366 	}
 367 	if (exists $bad{$tag}{$channel_text}{$host_text}) {
 368 		Irssi::timeout_remove($bad{$tag}{$channel_text}{$host_text});
 369 		delete $bad{$tag}{$channel_text}{$host_text};
 370 		if (not keys %{$bad{$tag}{$channel_text}}) {
 371 			delete $bad{$tag}{$channel_text};
 372 		}
 373 		if (not keys %{$bad{$tag}}) {
 374 			delete $bad{$tag};
 375 		}
 376 	}
 377 }
 378 
parse_channels()

Examine autovoice_channels and return a hash reference. Each key is a channel name, lowercased. Each value is a hash with one to three keys, 'server', 'chatnet', and/or 'plain'. If server, it holds an array ref with all servers on which the channel is autovoice. If chatnet, it holds an array ref with all the chatnets on which the channel is autovoice. If plain, it just has the value 1.

 384 
 385 sub parse_channels {
 386 	my $chanstring = lc Irssi::settings_get_str('autovoice_channels');
 387 	$chanstring =~ s/^\s+//;
 388 	$chanstring =~ s/\s+$//;
 389 	my @fields = split /\s+,\s*/, $chanstring;
 390 	my %hash;
 391 	keys %hash  = scalar @fields;
 392 	for (@fields) {
 393 		if (/\s=>/) {
 394 			my ($channel, $chatnet) = split /\s+=>\s*/, $_, 2;
 395 			add_channel_to_parsed(\%hash, $channel, $chatnet, undef);
 396 		} elsif (/\s\@/) {
 397 			my ($channel, $server) = split /\s+\@\s*/, $_, 2;
 398 			add_channel_to_parsed(\%hash, $channel, undef, $server);
 399 		} else {
 400 			my ($channel) = /(\S+)/;
 401 			add_channel_to_parsed(\%hash, $channel, undef, undef);
 402 		}
 403 	}
 404 	return \%hash;
 405 }
 406 
deparse_channels($hashr)

Take a hash ref like that produced by parse_channels and convert it into a string suitable for autovoice_channels

 412 
 413 sub deparse_channels {
 414 	my $hashr = shift;
 415 	my @fields;
 416 	for my $channel (keys %$hashr) {
 417 		my $s = $channel;
 418 		push(@fields, $s) if exists $hashr->{$channel}->{plain};
 419 		if (exists $hashr->{$channel}->{server}) {
 420 			for (@{$hashr->{$channel}->{server}}) {
 421 				push(@fields, $s.' @ '.$_);
 422 			}
 423 		}
 424 		if (exists $hashr->{$channel}->{chatnet}) {
 425 			for (@{$hashr->{$channel}->{chatnet}}) {
 426 				push(@fields, $s.' => '.$_);
 427 			}
 428 		}
 429 	}
 430 	return join ' , ', @fields;
 431 }
 432 
is_auto($channel_rec)

Returns 1 if $channel_rec is an autovoiced channel as defined by autovoice_channels, 0 otherwise.

 438 
 439 sub is_auto {
 440 	unless (Irssi::settings_get_bool('autovoice')) {
 441 		return 0;
 442 	}
 443 	my $channel_rec = shift;
 444 	my $channel_text = lc $channel_rec->{name};
 445 	my $parsedchannels = parse_channels();
 446 	return 0 unless exists $parsedchannels->{$channel_text};
 447 	if (exists $parsedchannels->{$channel_text}->{plain}) {
 448 		return 1;
 449 	} elsif (exists $parsedchannels->{$channel_text}->{chatnet}) {
 450 		#Irssi::print("looking at chatnet @{$parsedchannels->{$channel_text}->{chatnet}}");
 451 		for (@{$parsedchannels->{$channel_text}->{chatnet}}) {
 452 			return 1 if $_ eq lc $channel_rec->{server}->{chatnet};
 453 		}
 454 		return 0;
 455 	} else {
 456 		for (@{$parsedchannels->{$channel_text}->{server}}) {
 457 			return 1 if $_ eq lc $channel_rec->{server}->{address};
 458 		}
 459 		return 0;
 460 	}
 461 }
 462 
 463 our %commands = (
 464 					dump => \&dump,
 465 					add => \&add,
 466 					remove => \&remove,
 467 					flush => \&flush,
 468 				);
 469 
autovoice_cmd($data, $server, $witem)

Irssi command handler which dispatches all the /autovoice * commands. Autovoice commands are given ($server_rec, $witem, @args), where @args is the result of split ' ', $data minus the first element (``autovoice''). Note that the case of @args is not changed.

 475 
 476 sub autovoice_cmd {
 477 	my ($data, $server, $witem) = @_;
 478 	my ($cmd, @args) = (split ' ', $data);
 479 	$cmd = lc $cmd;
 480 	if (exists $commands{$cmd}) {
 481 		$commands{$cmd}->($server, $witem, @args)
 482 	} else {
 483 		Irssi::print("No such command: autovoice $cmd");
 484 	}
 485 }
 486 
dump($server_rec, $witem, @args)

Invoked as /autovoice dump, this requires Data::Dumper and dumps the blacklist hash to the current window. @args and $server_rec are ignored.

 492 
 493 sub dump {
 494 	require Data::Dumper;
 495 	my $witem = $_[1];
 496 	my $string = Data::Dumper->Dump([\%bad], ['bad']);
 497 	chomp $string;
 498 	if ($witem) {
 499 		$witem->print($string);
 500 	} else {
 501 		Irssi::print($string);
 502 	}
 503 }
 504 
add($server_rec, $witem, @args)

Invoked as /autovoice add (args). This adds channels to autovoice_channels. See autovoice add in COMMANDS for usage.

 510 
 511 sub add {
 512 	my ($server_rec, $witem, @args) = @_;
 513 	@args = map {lc} @args;
 514 	my $parsedchannels = parse_channels();
 515 	my ($server, $chatnet, $channel);
 516 	for (my $i = 0; $i < @args; ++$i) {
 517 		if ($args[$i] eq '-ircnet') {
 518 			if (defined $chatnet) {
 519 				Irssi::print("autovoice add: warning: -ircnet given twice, using the second value.");
 520 			}
 521 			$chatnet = $args[$i+1];
 522 			splice(@args, $i, 1)
 523 		} elsif ($args[$i] eq '-server') {
 524 			if (defined $server) {
 525 				Irssi::print("autovoice add: warning: -server given twice, using the second value.");
 526 			}
 527 			$server = $args[$i+1];
 528 			splice(@args, $i, 1);
 529 		} else {
 530 			if (defined $channel) {
 531 				Irssi::print("autovoice add: warning: more than one channel specified, using the last one.");
 532 			}
 533 			$channel = $args[$i];
 534 			$channel = '#'.$channel
 535 				unless $server_rec->ischannel($channel);
 536 		}
 537 	}
 538 	unless (defined $channel) {
 539 		Irssi::print("autovoice add: no channel specified");
 540 		return;
 541 	}
 542 	add_channel_to_parsed($parsedchannels, $channel, $chatnet, $server);
 543 	Irssi::settings_set_str('autovoice_channels' =>
 544 			deparse_channels($parsedchannels));
 545 	if ($witem) {
 546 		$witem->command("set autovoice_channels");
 547 	} else {
 548 		Irssi::command("set autovoice_channels");
 549 	}
 550 }
 551 
add_channel_to_parsed($parsedchannels, $channel, $chatnet, $server)

Adds a channel to a hash ref like that returned by &parse_channels. If $chatnet is defined but $server is not, restrict it to the chatnet. If $server is defined but $chatnet is not, restrict it to the server. If both are defined, add to channels, one restricted to the server and the other to the chatnet. (Both with the same name) If neither is defined, do not restrict the channel to a chatnet or server.

 557 
 558 sub add_channel_to_parsed {
 559 	my ($parsedchannels, $channel, $chatnet, $server) = @_;
 560 	if (defined $chatnet) {
 561 		push @{$parsedchannels->{$channel}->{chatnet}}, $chatnet;
 562 	} 
 563 	if (defined $server) {
 564 		push @{$parsedchannels->{$channel}->{server}}, $server;
 565 	} 
 566 	if (not defined($chatnet) and not defined($server)) {
 567 		$parsedchannels->{$channel}->{plain} = 1;
 568 	}
 569 }
 570 
remove($server_rec, $witem, @args)

Invoked as

        /autovoice remove [-ircnet IRCNET] [-server SERVER] #chan1 [-ircnet IRCNET] [-server SERVER] #chan2

Removes all channels matching those specified. An -ircnet or -server option only applies to the channel following it, and must be specified before its channel name. A channel without -ircnet or -server options removes all channels with that name.

 580 
 581 sub remove {
 582 	my ($server_rec, $witem, @args) = @_;
 583 	my %parsedchannels = %{parse_channels()};
 584 	my ($wantserver, $wantchatnet, $server, $chatnet);
 585 	for (@args) {
 586 		$_ = lc;
 587 		if ($wantserver) {
 588 			$wantserver = 0;
 589 			$server = $_;
 590 		} elsif ($wantchatnet) {
 591 			$wantchatnet = 0;
 592 			$chatnet = $_;
 593 		} elsif ($_ eq '-server') {
 594 			$wantserver = 1;
 595 		} elsif ($_ eq '-ircnet') {
 596 			$wantchatnet = 1;
 597 		} elsif (exists $parsedchannels{$_}) {
 598 			my $chan = $_;
 599 			if (defined $server and exists $parsedchannels{$chan}{server}) {
 600 				@{$parsedchannels{$chan}{server}} = grep {$_ ne $server} @{$parsedchannels{$chan}{server}};
 601 			}
 602 			if (defined $chatnet and exists $parsedchannels{$chan}{chatnet}) {
 603 				@{$parsedchannels{$chan}{chatnet}} = grep {$_ ne $chatnet} @{$parsedchannels{$chan}{chatnet}};
 604 			}
 605 			if (not defined $server and not defined $chatnet) {
 606 				delete $parsedchannels{$chan};
 607 			} else {
 608 				if (exists $parsedchannels{$chan}{server} and not @{$parsedchannels{$chan}{server}}) {
 609 					delete $parsedchannels{$chan}{server};
 610 				}
 611 				if (exists $parsedchannels{$chan}{chatnet} and not @{$parsedchannels{$chan}{chatnet}}) {
 612 					delete $parsedchannels{$chan}{chatnet};
 613 				}
 614 			}
 615 		}
 616 	}
 617 	Irssi::settings_set_str('autovoice_channels' =>
 618 			deparse_channels(\%parsedchannels));
 619 	if ($witem) {
 620 		$witem->command("set autovoice_channels");
 621 	} else {
 622 		Irssi::command("set autovoice_channels");
 623 	}
 624 }
 625 
flush($server_rec, $witem, @args)

Flush the badlist.

 631 
 632 sub flush {
 633 	%bad = ();
 634 }
 635 
get_cycletime()

Checks autovoice_cycletime and returns the cycle time in milliseconds.

 641 
 642 sub get_cycletime {
 643 	1000 * Irssi::settings_get_int("autovoice_cycletime");
 644 }
 645 
get_voiceops()

Return the value of autovoice_voice_ops

 651 
 652 sub get_voiceops {
 653 	Irssi::settings_get_bool("autovoice_voice_ops");
 654 }
 655 
get_useident()

Return the value of autovoice_use_ident

 661 
 662 sub get_useident {
 663 	Irssi::settings_get_bool("autovoice_use_ident");
 664 }
 665 
 669 
 670 Irssi::signal_add_first('message part', 'message_part');
 671 Irssi::signal_add_first('message quit', 'message_quit');
 672 Irssi::signal_add_first('message kick', 'message_kick');
 673 Irssi::signal_add_last('massjoin', 'massjoin');
 674 Irssi::settings_add_str('autovoice', 'autovoice_channels' => "");
 675 Irssi::settings_add_int('autovoice', 'autovoice_cycletime' => 600);
 676 Irssi::settings_add_bool('autovoice', 'autovoice_voice_ops' => 0);
 677 Irssi::settings_add_bool('autovoice', 'autovoice_use_ident' => 0);
 678 Irssi::settings_add_bool('autovoice', 'autovoice' => 1);
 679 Irssi::command_bind(autovoice => 'autovoice_cmd');