##
# /toggle whitelist_notify [default ON]
# Print a message in the status window if someone not on the whitelist messages us
#
# /toggle whitelist_log_ignored_msgs [default ON]
# if this is on, ignored messages will be logged to ~/.irssi/whitelist.log
#
# /set whitelist_nicks phyber etc
# nicks that are allowed to msg us (whitelist checks for a valid nick before a valid host)
#
# /toggle whitelist_nicks_case_sensitive [default OFF]
# do we care which case nicknames are in?
#
# Thanks to Geert for help/suggestions on this script
#
# Karl "Sique" Siegemund's addition:
# Managing the whitelists with the /whitelist command:
#
# /whitelist add nick <list of nicks>
# puts new nicks into the whitelist_nicks list
#
# /whitelist add host <list of hosts>
# puts new hosts into the whitelist_hosts list
#
# /whitelist add chan[nel] <list of channels>
# puts new channels into the whitelist_channels list
#
# /whitelist add net[work] <list of chatnets/servers>
# puts new chatnets or irc servers into the whitelist_networks list
#
# /whitelist del nick <list of nicks>
# removes the nicks from whitelist_nicks
#
# /whitelist del host <list of hosts>
# removes the hosts from whitelist_hosts
#
# /whitelist del chan[nel] <list of channels>
# removes the channels from whitelist_channels
#
# /whitelist del net[work] <list of chatnets/servers>
# removes the chatnets or irc servers from whitelist_networks
#
# Instead of the 'del' modifier you can also use 'remove':
# /whitelist remove [...]
#
# /whitelist nick
# shows the current whitelist_nicks
#
# /whitelist host
# shows the current whitelist_hosts
#
# /whitelist chan[nel]
# shows the current whitelist_channels
#
# /whitelist net[work]
# shows the current whitelist_networks
#
# Additional feature for nicks, channels and hosts:
# You may use <nick>@<network>/<ircserver>, <host>@<network>/<ircserver>
# and <channel>@<network>/<ircserver> to restrict the whitelisting to the
# specified network or ircserver.
#
# The new commands are quite verbose. They are so for a reason: The commands
# should be easy to remember and self explaining. If someone wants shorter
# commands, feel free to use 'alias'.
##
# /whitelist upgrade
# convert the old style settings to the new hash/config file based settings.
# you MUST run this if you haven't generated a config file yet.
#
# /whitelist show
# shows you all of the whitelisted entries.

use strict;
use Irssi;
use Irssi::Irc;
use IO::File;

use vars qw($VERSION %IRSSI);
$VERSION = "1.1";
%IRSSI = (
	authors		=> "David O\'Rourke, Karl Siegemund",
	contact		=> "phyber \[at\] #irssi, q \[at\] spuk.de",
	name		=> "whitelist",
	description	=> "Whitelist specific nicks or hosts and ignore messages from anyone else.",
	licence		=> "GPLv2",
	changed		=> "20/03/2007 17:11 GMT"
);

# location of the settings file
my $settings_file = Irssi::get_irssi_dir.'/whitelist.conf';
# This hash stores our various whitelists.
my %whitelisted;

# A mapping to convert simple regexp (* and ?) into Perl regexp
my %htr = ( );
foreach my $i (0..255) {
	my $ch = chr($i);
	$htr{$ch} = "\Q$ch\E";
}
$htr{'?'} = '.';
$htr{'*'} = '.*';

# A list of settings we can use and change
my %types = (
	'nick'		=> 'nicks',
	'host'		=> 'hosts',
	'chan'		=> 'channels',
	'channel'	=> 'channels',
	'net'		=> 'networks',
	'network'	=> 'networks',
);

sub host_to_regexp {
	my ($mask) = @_;
	$mask = lc_host($mask);
	$mask =~ s/(.)/$htr{$1}/g;
	return $mask;
}

sub lc_host {
	my ($host) = @_;
	$host =~ s/(.+)\@(.+)/sprintf("%s@%s", $1, lc($2));/eg;
	return $host;
}

# Show the current config
sub print_config {
	foreach my $listtype (keys %whitelisted) {
		my $str = join ' ', @{$whitelisted{$listtype}};
		Irssi::print "Whitelisted $listtype: $str";
	}
}

# Read in the whitelist.conf
sub read_config {
	# nicks, hosts, channels, networks
	my $f = IO::File->new($settings_file, 'r');
	if (!defined $f) {
		Irssi::print "Couldn't open $settings_file for reading. Do you need to generate a config file with '/whitelist upgrade' ?";
		return;
	}

	while (<$f>) {
		chomp;
		my ($listtype, @list) = split / /, $_;

		# Make sure there is no duplicate weirdness
		@{$whitelisted{$listtype}} = unique(@list); #map { $_ } @list;
	}
	$f = undef;
}

# Write out the whitelist.conf
sub write_config {
	my $f = IO::File->new($settings_file, 'w');
	die "Couldn't open $settings_file for writing" if (!defined $f);

	foreach my $listtype (keys %whitelisted) {
		# Make sure we arn't writing duplicates
		@{$whitelisted{$listtype}} = unique(@{$whitelisted{$listtype}});

		# Now write that list to the config.
		my $str = join ' ', @{$whitelisted{$listtype}};
		print {$f} "$listtype $str\n";
	}
	$f = undef;
}

# convert old settings to new settings (/whitelist upgrade)
sub old2new {
	my $nicks	= Irssi::settings_get_str('whitelist_nicks');
	my $hosts	= Irssi::settings_get_str('whitelist_hosts');
	my $channels	= Irssi::settings_get_str('whitelist_channels');
	my $networks	= Irssi::settings_get_str('whitelist_networks');

	# Convert nick whitelist
	foreach my $nick (split /\s+/, $nicks) {
		next if not length $nick;
		push @{$whitelisted{'nicks'}}, $nick;
	}

	# Convert host whitelist
	foreach my $host (split /\s+/, $hosts) {
		next if not length $host;
		push @{$whitelisted{'hosts'}}, $host;
	}

	# Convert channel whitelist
	foreach my $channel (split /\s+/, $channels) {
		next if not length $channel;
		push @{$whitelisted{'channels'}}, $channel;
	}

	# Convert network whitelist
	foreach my $network (split /\s+/, $networks) {
		next if not length $network;
		push @{$whitelisted{'networks'}}, $network;
	}

	# now make sure entries in those whitelists are unique.
	foreach my $listtype (keys %whitelisted) {
		@{$whitelisted{$listtype}} = unique(@{$whitelisted{$listtype}});
	}

	write_config();
}

# return a unique array.
sub unique {
	my %seen;
	return grep { !$seen{$_}++ } @_;
}

# This one gets called from IRSSI if we get a private message (PRIVMSG)
sub whitelist_check {
	my ($server, $msg, $nick, $address) = @_;
	# Some /set settings.
	my $warning		= Irssi::settings_get_bool('whitelist_notify');
	my $casesensitive	= Irssi::settings_get_bool('whitelist_nicks_case_sensitive');
	my $logging		= Irssi::settings_get_bool('whitelist_log_ignored_msgs');
	my $logfile		= Irssi::get_irssi_dir.'/whitelist.log';

	my $hostmask		= "$nick!$address";

	my $tag			= $server->{chatnet};
	$tag			= $server->{tag} unless defined $tag;
	$tag			= lc($tag);

	# Handle servers first, because they are the most significant,
	# Nicks, Channels and Hostmasks are always local to a network
	foreach my $netentry (@{$whitelisted{'networks'}}) {
		# copy it so we don't modify the array
		my $network = $netentry;
		# Change it to lower case
		$network = lc($network);
		# Kludge. Sometimes you get superfluous '', you have to ignore
		next if ($network eq '');
		# Rewrite simplified regexp (* and ?) to Perl regexp
		$network =~ s/(.)/$htr{$1}/g;
		# Either the server tag matches
		return if ($tag =~ /$network/);
		# Or its address
		return if ($server->{address} =~ /$network/);
	}

	# Nicks are the easiest to handle with the least computational effort.
	# So do them before hosts and networks.
	foreach my $whitenickentry (@{$whitelisted{'nicks'}}) {
		# copy it so we don't modify the array
		my $whitenick = $whitenickentry;

		if (!$casesensitive) {
			$nick = lc($nick);
			$whitenick = lc($whitenick);
		}
		# Simple check first: Is the nick itself whitelisted?
		return if ($nick eq $whitenick);
		# Second check: We have to look if the nick was localized to a network
		# or irc server. So we have to look at <nick>@<network> too.
		($whitenick, my $network) = split /@/, $whitenick, 2;
		# Ignore nicks without @<network>
		next if !defined $network;
		# Convert simple regexp to Perl regexp
		$network =~ s/(.)/$htr{$1}/g;
		# If the nick matches...
		if ($nick eq $whitenick) {
			# ...allow if the server tag is right...
			return if ($tag =~ /$network/);
			# ...or the server address matches
			return if ($server->{address} =~ /$network/);
		}
	}
	
	# Hostmasks are somewhat more sophisticated, because they allow wildcards
	foreach my $whitehostentry (@{$whitelisted{'hosts'}}) {
		# copy it so we don't modify the array
		my $whitehost = $whitehostentry;
		# Kludge, sometimes you get ''
		next if ($whitehost eq '');
		# First reconvert simple regexp to Perl regexp
		$whitehost = host_to_regexp($whitehost);
		# Allow if the hostmask matches
		return if ($hostmask =~ /$whitehost/);
		# Check if hostmask is localized to a network
		(my $whitename, $whitehost, my $network) = split /@/, $whitehost, 3;
		# Ignore hostmasks without attached network
		next if !defined $network;
		# We don't need to convert the network address again
		# $network =~ s/(.)/$htr{$1}/g;
		# But we have to reassemble the hostmask
		$whitehost = "$whitename\@$whitehost";
		# If the hostmask matches...
		if ($hostmask eq $whitehost) {
			# ...allow if the server tag is ok...
			return if ($tag =~ /$network/);
			# ... or the server address
			return if ($server->{address} =~ /$network/);
		}
	}

	# Channels require some interaction with the server, so we do them last,
	# hoping that some ACCEPT cases are already done, thus saving computation
	# time and effort
	foreach my $channel (@{$whitelisted{'channels'}}) {
		# Check if we are on the specified channel
		my $chan = $server->channel_find($channel);
		# If yes...
		if (defined $chan) {
			# Check if the nick in question is also on that channel
			my $chk = $chan->nick_find($nick);
			# Allow the message
			return if defined $chk;
		}
		# Check if we are talking about a localized channel
		($chan, my $network) = split /@/, $_, 2;
		# Ignore not localized channels
		next if !defined $network;
		# Convert simple regexp to Perl regexp
		$network =~ s/(.)/$htr{$1}/g;
		# Ignore channels from a differently tagged server or from a different
		# address
		next if (!($tag =~ /$network/ || $server->{address} =~ /$network/));
		# Check if we are on the channel
		$chan = $server->channel_find($chan);
		# Ignore if not
		next unless defined $chan;
		# Check if $nick is on that channel too
		my $chk = $chan->nick_find($nick);
		# Allow if yes
		return if defined $chk;
	}
	
	# Do we want a notice about this message attempt?
	if ($warning) {
		Irssi::print "[$tag] $nick [$address] attempted to send private message.";
	}
	
	# Do we want to make a log entry for it?
	if ($logging) {
		my $f = IO::File->new($logfile, '>>');
		return if (!defined $f);
		print {$f} localtime().": [$tag] $nick [$address]: $msg\n";
		$f = undef;
	}

	# stop if the message isn't from a whitelisted address
	Irssi::signal_stop();
	return;
}

sub usage {
	Irssi::print "Usage: whitelist (add|del|remove) (nick|host|chan[nel]|net[work]) <list>";
	Irssi::print "       whitelist (nick|host|chan[nel]|net[work])";
	Irssi::print "       whitelist upgrade";
	Irssi::print "       whitelist show";
}

# This is bound to the /whitelist command
sub whitelist_cmd {
	my ($args, $server, $winit) = @_;
	my ($cmd, $type, $rest) = split /\s+/, $args, 3;

	# What type of settings we want to change?
	my $listtype = $types{$type};

	# If we didn't get a syntactically correct command, put out an error
	if (!defined $listtype && defined $type) {
		usage();
		return;
	} 
	
	# What are we doing?
	if ($cmd eq 'add') {
		# split $rest into a list.
		my @list = split /\s+/, $rest;

		# Add the entries to the whitelist and then make sure it's unique
		foreach my $entry (@list) {
			push @{$whitelisted{$listtype}}, $entry;
		}
		# Make sure that the array only contains unique entries
		@{$whitelisted{$listtype}} = unique(@{$whitelisted{$listtype}})
	}
	elsif ($cmd eq 'del' || $cmd eq 'remove') {
		# Escape all letters to protect the Perl Regexp special characters
		$rest =~ s/(.)/$htr{$1}/g;

		# Make a list of things we want removing.
		my @list = split /\s+/, $rest;

		# Use grep to remove the list of things we don't want anymore.
		foreach my $removal (@list) {
			@{$whitelisted{$listtype}} = grep {!/^$removal$/} @{$whitelisted{$listtype}};
		}
	}
	elsif ($cmd eq 'upgrade') {
		Irssi::print "Converting old style /settings to new config file based settings";
		old2new();
		read_config();
		print_config();
		return;
	}
	elsif ($cmd eq 'show') {
		print_config();
		return;
	}
	elsif (!defined $type) {
		# Look if we just want to see the current values
		$listtype = $types{$cmd};
		if (defined $listtype) {
			# Print them
			Irssi::print "Whitelist ${cmd}s: ".join ' ', @{$whitelisted{$listtype}};
		}
		else {
			# Or give error message
			usage();
		}
		return;
	}
	else {
		# If we felt through until here, something went wrong
		usage();
		return;
	}
	# Display the changed value and store it in the settings
	Irssi::print "Whitelist ${type}s: ".join ' ', @{$whitelisted{$listtype}};
	# Save the new settings
	write_config();
	return;
}

Irssi::settings_add_bool('whitelist', 'whitelist_notify' => 1);
Irssi::settings_add_bool('whitelist', 'whitelist_log_ignored_msgs' => 1);
Irssi::settings_add_bool('whitelist', 'whitelist_nicks_case_sensitive' => 0);

foreach (keys(%types)) {
	Irssi::settings_add_str('whitelist', 'whitelist_'.$types{$_}, '');
}

Irssi::signal_add_first('message private', \&whitelist_check);

Irssi::command_bind('whitelist', \&whitelist_cmd);

# Read the config
\&read_config();
#########################
####### Changelog #######
### 1.1: David O'Rourke
# Made a small function named unique() which checks that entries in an array are unique.
# Modified various parts of the script to use the new unique() function.
# Fixed the check function so that it doesn't modify the array it's checking. (It was lc()ing things).
# Removed some old commented out code.  Added a few comments to various things.
### 1.0: David O'Rourke
# Changed how whitelists are stored.  We no longer use the settings_*_str for them.
# We now store them in a hash and write/read a config file.
# Added '/whitelist old2new' function, for converting to the new style list.
# Added '/whitelist show' for showing everything that's been whitelisted.
### 0.9g: David O'Rourke
# Cleanups.
### 0.9f: David O'Rourke
# Cleanups.
### 0.9e: David O'Rourke
# Changed print -> Irssi::print
# Fixed '' in $whitehost
#########################
# 0.9d: David O'Rourke
# General cleanup of script.
# Removed pointless function timestamp()
# Removed pointless global variables $tstamp, $whitenick, $whitehost
# Created whitelist logging directory in ~/.irssi with option to rotate log daily.
# Fixed comparison of whitelist_networks to $tag.  $tag was being lowercased, whitelist_networks was not.
