#!/usr/bin/perl -w

$VERSION = "0.1";
%IRSSI = (
	authors 	=> 'Michał Mirosław',
	contact 	=> 'mirqus@lagoon.freebsd.lublin.pl',
	name		=> 'join_quieter',
	description	=> 'Hides join/parts for silent users (ones that don\'t say anything on public)'
);

use Irssi;
use Irssi::Irc;

my $maskfile = "$ENV{HOME}/.irssi/loud_nicks";

Irssi::theme_register([
	'extended_join', '{channick_hilight $0} {chanhost_hilight $1} has joined {channel $2} {comment $3} ago',
	'only_visited', '{channick $0} {chanhost $1} visited {channel $2} for {hilight $4} and parted {reason $3}',
	'only_visited_quit', '{channick $0} {chanhost $1} visited IRC for {hilight $3} and quit {reason $2}',
	'kicked_shit', 'shit/{channel $2} {channick $0} {chanhost $1} by {nick $3} {reason $4}',
#	'kicked_shit', '{channick $0} {chanhost $1} was shitted at {channel $2} by {nick $3} {reason $4}',
	'join_for_invite_q', '{nick $0} {nickhost $1} joined for invitespam:{hilight $2}',
	'join_for_invite_l', '{nick $0} {nickhost $1} joined:{hilight $2}',
	'file_error', 'Could not open file {hilight $0}: {error $1}',
	'file_loaded', '{hilight $0} usermasks loaded from {hilight $1}',
#FIXME: use irssi standard theme for this, since it's the same, and should be the same
	'nick_changed', '{channick $0} is now known as {channick_hilight $1} *',
	'quit', '{channick $0} {chanhost $1} has quit {reason $2} *'
]);

# usermasks that won't be quieted
my $loud_exceptions = "";

### people list

my %people = ();

sub add_person {
	my ($server, $nick, $address, $channel, $time) = @_;
	unless (exists $people{$server}->{$nick}) {
		$people{$server}->{$nick} = { address => $address };
	} else { # debug only, should not happen
		my $addr = $people{$server}->{$nick}->{address};
		Irssi::print("$nick changed address: $addr -> $address", MSGLEVEL_CLIENTERROR)
			if $addr ne $address;
	}
	my $ref = $people{$server}->{$nick};
	$ref->{channels}->{$channel} = $time;
	$ref->{chflags}->{$channel} = 0;
}

# find and remove
# returns join time if channel specified, whole nick hash ref otherwise
sub remove_person {
	my ($server, $nick, $channel) = @_;
	return undef unless exists $people{$server}->{$nick};
	my $ref = $people{$server}->{$nick};

	unless (defined $channel) {
		delete $people{$server}->{$nick};
		return $ref;
	}

	return undef unless exists $ref->{channels}->{$channel};
	my $time = $ref->{channels}->{$channel};
	delete $ref->{channels}->{$channel};
	delete $ref->{chflags}->{$channel};
	my $addr = $ref->{address};
	delete $people{$server}->{$nick}
		unless scalar(keys %{$ref->{channels}});
	return $time unless wantarray;
	return ($time, $addr);
}

#### helper functions

# makes human readable interval from seconds count
sub time_form {
	use integer;
	my ($time) = @_;
	my $txt = "";

	$txt = ($time % 60) . " sec $txt" if $time % 60;
	$time /= 60;
	$txt = ($time % 60) . " min $txt" if $time % 60;
	$time /= 60;
	$txt = ($time % 24) . " h $txt"   if $time % 24;
	$time /= 24; 
	$txt = $time . " days $txt" if $time > 0;

	chop $txt;	# last space
	$txt = "a moment" if $txt eq '';
	return $txt;
}

# prints extended join message
sub extended_join {
	my ($server, $channel, $nick, $address, $time) = @_;
	$time = time - $time;
	$channel = $server->channel_find($channel) unless ref $channel;
	return unless defined $channel;
	$channel->printformat(MSGLEVEL_JOINS, 'extended_join',
		$nick, $address, $channel->{name}, time_form($time));
}

# processes (prints) delayed join
sub delayed_join {
	my ($server, $channel, $nick, $address) = @_;
	my ($time, $addr) = remove_person($server->{tag}, $nick, $channel->{name});

	return unless defined $time;
	$address = $addr unless defined $address;
	extended_join($server, $channel, $nick, $address, $time);
}

#### events

sub channel_destroyed {
	my ($channel) = @_;
	foreach my $nick (keys %{ $people{$channel->{server}->{tag}} }) {
		remove_person($channel->{server}->{tag}, $nick, $channel->{name});
	}
}

sub server_disconnected {
	my ($server) = @_;
	delete $people{$server->{tag}};
}

sub message_join {
	my ($server, $channel, $nick, $address) = @_;
	return if $server->masks_match($loud_exceptions, $nick, $address);
	return if $nick eq $server->{nick};

	add_person($server->{tag}, $nick, $address, $channel, time);
	Irssi::signal_stop();
}

# misc/quiet_visits
Irssi::settings_add_bool("misc", "quiet_visits", 0);
# when on, will not print messages about quiet visitors

sub message_part {
	my ($server, $channel, $nick, $address, $reason) = @_;
	my $time = remove_person($server->{tag}, $nick, $channel);
	return unless defined $time;

	$time = time - $time;
	my $chanref = $server->channel_find($channel);
	$chanref->printformat(MSGLEVEL_PARTS, 'only_visited',
		$nick, $address, $channel, $reason, time_form($time))
		unless Irssi::settings_get_bool("quiet_visits");
	Irssi::signal_stop();
}

sub message_quit {
	my ($server, $nick, $address, $reason) = @_;

	return unless exists $people{$server->{tag}};
	return unless exists $people{$server->{tag}}->{$nick};

	my $ref = remove_person($server->{tag}, $nick);

	foreach my $channel ($server->channels()) {
		next unless defined $channel->nick_find($nick);
		if (defined $ref->{channels}->{$channel->{name}}) {
			my $time = time - $ref->{channels}->{$channel->{name}};
			$channel->printformat(MSGLEVEL_QUITS, 'only_visited_quit',
				$nick, $address, $reason, time_form($time))
				unless Irssi::settings_get_bool("quiet_visits");
		} else {
			$channel->printformat(MSGLEVEL_QUITS, 'quit',
				$nick, $address, $reason);
		}
	}
	Irssi::signal_stop();
}

# int misc/quiet_shitkick
Irssi::settings_add_int("misc", "quiet_shitkick", 2);
# valid settings:
# -1 loud shitting (prints delayed join and shitkick)
# 0 do nothing (irssi will print standard kick message)
# 1 silent shitting (prints nothing)
# 2 semiloud (prints only shitkick)

sub message_kick {
	my ($server, $channel, $nick, $kicker, $address, $reason) = @_;
	my ($time, $addr) = remove_person($server->{tag}, $nick, $channel);
	return unless defined $time;

	my $printmode = Irssi::settings_get_int("quiet_shitkick");
	return unless $printmode;

	extended_join($server, $channel, $nick, $addr, $time) if $printmode < 0;
	$chanref->printformat(MSGLEVEL_PARTS, 'kicked_shit',
		$nick, $addr, $channel, $kicker, $reason) if $printmode != 1;
	Irssi::signal_stop();
}

# bool misc/quiet_nickchange
Irssi::settings_add_bool("misc", "quiet_nickchange", 1);
# when turned off nichchange will behave like public message

sub message_nick {
	my ($server, $nick, $oldnick, $address) = @_;
	my $ref = remove_person($server->{tag}, $oldnick);

	foreach my $channel ($server->channels()) {
		next unless defined $channel->nick_find($nick) or defined $channel->nick_find($oldnick);
		my $time = undef;
		$time = $ref->{channels}->{$channel->{name}}
			if exists $ref->{channels}->{$channel->{name}};
		if (defined $time and not Irssi::settings_get_bool("quiet_nickchange")) {
			extended_join($server, $channel, $oldnick, $address, $time);
			$time = undef;
		}
		$channel->printformat(MSGLEVEL_NICKS, 'nick_changed', $oldnick, $nick)
			unless defined $time;
	}
	$people{$server->{tag}}->{$nick} = $ref;
	Irssi::signal_stop();
}

sub message_public {
	my ($server, $msg, $nick, $address, $target) = @_;
	delayed_join($server, $target, $nick, $address);
}

sub message_topic {
	my ($server, $channel, $topic, $nick, $address) = @_;
	delayed_join($server, $channel, $nick, $address);
}

# misc/quiet_invitespam
Irssi::settings_add_bool("misc", "quiet_invitespam", 0);
# when on, doesn't print invites from people that joined channel and sat silently

sub message_invite {
	my ($server, $channel, $nick, $address) = @_;
	my $ref = remove_person($server->{tag}, $nick);
	return unless defined $ref;
	$people{$server->{tag}}->{$nick} = $ref;

	my $joinlist = "";

	foreach my $chan (keys %{$ref->{channels}}) {
		my $time = time - $ref->{channels}->{$chan};
		$joinlist .= " $chan [" . time_form($time) . " ago]";
	}
	if (Irssi::settings_get_bool("quiet_invitespam")) {
		$server->printformat('', MSGLEVEL_INVITES, 'join_for_invite_q',
			$nick, $address, $joinlist);
		Irssi::signal_stop()
	} else {
		$server->printformat('', MSGLEVEL_INVITES, 'join_for_invite_l',
			$nick, $address, $joinlist);
	}
}

sub irc_mode_split {
	my ($mode) = @_;
	my @modes = ();
	my @params = ();

	foreach(split(/ /, $mode)) {
		push(@params, $_), next unless m/^[-+]/;
		my @chars = split(//, $_);
		my $state = 0;
		while (scalar(@chars)) {
			my $c = shift(@chars);
			$state = $c, next if $c eq '+' or $c eq '-';
			push @modes, "$state$c";
		}
	}

	my @result = ();
	foreach(@modes) {
		push @result, $_;
		push @result, shift(@params) if m/[beklovO]$/;
	}
	return @result;
}

sub message_irc_mode {
	my ($server, $channel, $nick, $address, $mode) = @_;
	my @modes = irc_mode_split($mode);

	# FIXME shitban detect
	while (scalar(@modes)) {
		my $m = shift @modes;
		next if $m ne "+o" and $m ne "+v" and $m ne "+O";
		$m = shift @modes;
		delayed_join($server, $channel, $m);
	}
}

# load usermasks of loud people from file
sub load_loud_users {
	my ($file) = @_;

	unless (open(FH, "<$file")) {
		Irssi::printformat(MSGLEVEL_CLIENTERROR, 'file_error', $file, $!);
		return;
	}
	my @list = <FH>;
	close FH;
	chomp @list;
	s/#.*$//, s/[ \t]+//g foreach (@list);
	@list = grep($_ ne '', @list);
	Irssi::printformat(MSGLEVEL_CLIENTNOTICE, 'file_loaded', scalar(@list), $file);
	$loud_exceptions = join(' ', @list);
#	Irssi::print("\"$loud_exceptions\"", MSGLEVEL_CLIENTCRAP);
}

# /LOADJQ [file]
sub cmd_load {
	my ($param, $server, $witem) = @_;

	$param = $maskfile unless $param;
	&load_loud_users($param);
}

# /LISTJQ
sub cmd_list {
	my ($param, $server, $witem) = @_;

	Irssi::print("Loud people usermasks:", MSGLEVEL_CLIENTNOTICE);
	Irssi::print("  $_", MSGLEVEL_CLIENTNOTICE)
		foreach (split(/ /, $loud_exceptions));
}

# changes signal name to function name by changing spaces to underscores
# if first char of signal is '+', then it's removed and signal handler
# is installed using signal_add_first rather than signal_add
sub add_signals {
	my $lastf = undef;
	while (@_) {
		my $f = shift;
		my $first = ($f =~ s/^\+//);
		my $copy = ($f =~ s/^=//);
		my $s = $f;
		if ($copy) {
			$f = $lastf;
		} else {
			$f =~ s/ /_/g;
			$lastf = $f;
		}
		Irssi::signal_add($s, $f) unless $first;
		Irssi::signal_add_first($s, $f) if $first;
	}
}

Irssi::command_bind("loadjq", "cmd_load");
Irssi::command_bind("listjq", "cmd_list");
add_signals(
	"message join", "message part", "message kick", "message nick",
	"message quit", "message topic", "message invite", "message public",
	"=message irc action", "message irc mode", "channel destroyed", "server disconnected"
);
&load_loud_users($maskfile);
