#!/usr/bin/perl -w

#  IP2C
#  Copyright (C) 2011  Jernej Simončič
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Resolve user's city and country using Geo::IP. Requires both GeoIP and
# GeoIPCity databases (GeoLiteCity was tested).
#
#Changelog:
#
#2012-12-30: 0.3
# - ipv6 support
# - detect reserved IPv4 addresses
#
#2011-05-29: 0.2
# - if target isn't found in channel, treat as hostname
#
#2011-04-26: 0.1
# - first public version
#

use strict;
use warnings;
use utf8;
use Socket;
use Socket6;
use Geo::IP;

use constant VERSION => '0.3';

# channels in which to enable the script
my $chanmatch = qr=
					\#(?:some-channel|other-channel)
                  =ix;

my $last_response = 0; #time of last response to !ip2c
my %info;

my $gi4 = Geo::IP->open_type( GEOIP_CITY_EDITION_REV1 , GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE );
my $gic = Geo::IP->open( '/usr/share/GeoIP/GeoLiteCityv6.dat' , GEOIP_MEMORY_CACHE | GEOIP_CHECK_CACHE );

my $h_tmr; #cleanup timer hook handle

##
# server_hook: Handle messages from server
#
sub server_hook
{
	unless ($_[1][0] =~ /^:(([^!]+)!([^@]+)@(\S+)) PRIVMSG (\S+) :(.*)$/) {
		Xchat::print "Unexpected: ".$_[1][0];
		return Xchat::EAT_NONE;
	}

	my $mask = $1;
	my $nick = $2;
	my $ident = $3;
	my $host = $4;
	my $dest = lc($5);
	my $msg = $6;

	unless ($dest =~ $chanmatch) {
		return Xchat::EAT_NONE;
	}
	unless ($msg =~ /^.?([!\.](?:ip2c|stalk))(?:\s+(\S+)\s*)?$/) {
		return Xchat::EAT_NONE;
	}

	if (time - $last_response < 4) { #flood protection, also ensures no more than 1 !ip2c is tried at once
		Xchat::print "ip2c flood ignore";
		return Xchat::EAT_NONE;
	}
	$last_response = time;

	start_ip2c($dest,$2);

	return Xchat::EAT_NONE;
}

sub command_hook
{
	my $args = $_[1][1];
	my $where;

	CHECK: foreach my $channel (Xchat::get_list('channels')) {
		if ($channel->{channel} eq Xchat::get_info('channel')) {
			if ($channel->{type} != 2 && $channel->{type} != 3) {
				$where = undef;
				last CHECK;
			}
		}
	}

	start_ip2c($where,$args,1);
}

sub is_present($)
{
	my ($nick) = (@_);

	foreach (Xchat::get_list('users')) {
		if (Xchat::nickcmp($_->{nick},$nick) == 0) {
			return 1;
		}
	}
	return 0;
}

sub start_ip2c($$;$)
{
	my ($where,$who,$local) = @_;

	return unless defined $who;

	if (defined($where) && is_present($who)) { #don't bother checking users that aren't in the channel (TODO: should it respond with something when user isn't present?)
		$info{nick} = $who;
		$info{where} = $where;

		undef($info{host}); #clear old data
		undef($info{realhost});

		Xchat::command "whois $who";

		$h_tmr = Xchat::hook_timer(3000,\&reset_timer);
	} else {

		if ($who =~ /[.:]/) { #treat as IP/host
			do_ip2c($who,$who,$where);
		}

	}

}

sub do_ip2c($$$)
{
	my ($what,$host,$dest) = @_;

	my ($special,$girec);

	if ($host =~ /^\d+\.\d+\.\d+\.\d+$/) { #is it an IP?
		if ($host =~ /^127\./) {
			$special = 'loopback addresses';
		} elsif ($host =~ /^10\./ || $host =~ /^172\.(?:1[6-9]|2\d|3[01])\./ || $host =~ /^192\.168\./) {
			$special = 'private networks';
		} elsif ($host =~ /^0\./) {
			$special = 'broadcast messages to current network';
		} elsif ($host =~ /^100\.(?:6[4-9]|[789]\d|1[01]\d|12[0-7])\./) {
			$special = 'carrier-grade NAT';
		} elsif ($host =~ /^169\.254\./) {
			$special = 'automatically configured addresses';
		} elsif ($host =~ /^192\.0\.2\./ || $host =~ /^198\.51\.100\./ || $host =~ /^203\.0\.113\./) {
			$special = 'use in documentation and examples';
		} elsif ($host =~ /^192\.1[89]\./) {
			$special = 'testing internetwork communication';
		} elsif ($host =~ /^(?:22[4-9]|23\d)\./) {
			$special = 'multicast assignments';
		} elsif ($host =~ /^255\.255\.255\.255/) {
			$special = 'limited broadcast';
		} elsif ($host =~ /^(?:2[45]\d)\./) {
			$special = 'future use';
		} else {
			$girec = $gi4->record_by_addr($host);
		}
	} elsif ($host =~ /:/) {
		$girec = $gic->record_by_addr_v6($host);
	} else {
		$girec = $gic->record_by_name_v6($host);
		unless (defined($girec)) {
			$girec = $gi4->record_by_name($host);
		}
	}

	my $msg;
	if (defined($girec)) {
		if ($girec->city) {
			$msg = "\002".$what."\002 appears to be near \002".$girec->city."\002 in \002".$girec->country_name."\002.";
		} else {
			$msg = "\002".$what."\002 appears to be in \002".$girec->country_name."\002.";
		}
	} elsif (defined($special)) {
		$msg = "\002".$what."\002 is reserved for \002".$special."\002.";
	} else {
		$msg = "Couldn't determine where \002".$what."\002 is.";
	}

	Xchat::command "quote PRIVMSG ".$dest." :".$msg;
	Xchat::print '[IP2C output to channel] '.$msg, $dest;

}

sub whois_special
{
	my ($nick,$msg,$num) = @{$_[0]};

	return Xchat::EAT_NONE unless (defined($info{nick}) && Xchat::nickcmp($nick,$info{nick}) == 0); #make sure it's our whois

	if ($msg =~ /((?:\d+\.){3}\d+)\s*:\s*actually using host/) { #EFnet sends this
		$info{realhost} = $1;
	}

	return Xchat::EAT_ALL;
}

sub whois_name_line
{
	my ($nick,$user,$host,$fullname) = @{$_[0]};

	if (defined($info{nick}) && Xchat::nickcmp($nick,$info{nick}) == 0) {
		$info{nick} = $nick; # this corrects case (eg. if "!ip2c foo" was used, but user's nickname is actually Foo)
		$info{host} = $host;
		return Xchat::EAT_ALL;
	}

	return Xchat::EAT_NONE;
}

#last line of whois response, handle !ip2c response here
sub whois_end
{
	my ($nick) = @{$_[0]};

	return Xchat::EAT_NONE unless (defined($info{nick}) && Xchat::nickcmp($nick,$info{nick}) == 0); #make sure it's our whois

	Xchat::unhook($h_tmr);

	my $host;

	if ($info{realhost}) {
		$host = $info{realhost}; #prefer realhost when it's available
	} elsif($info{host}) {
		$host = $info{host};
	}

	if (!defined($host)) { #shouldn't actually happen, but better be safe than sorry
    
    	Xchat::command "quote PRIVMSG ".$info{where}." :\002Couldn't determine where \002".$info{nick}."\002 is\002.";
		Xchat::print "[IP2C output to channel] \002Couldn't determine where \002".$info{nick}."\002 is\002.",$info{where};
		return Xchat::EAT_ALL;

	}

	do_ip2c($info{nick}, $host, $info{where});

	undef $info{nick};

	return Xchat::EAT_ALL;
}

#suppress whois lines triggered by script
sub whois_suppress
{
	my ($nick) = @{$_[0]};

	if (defined($info{nick}) && Xchat::nickcmp($nick,$info{nick}) == 0) {
		return Xchat::EAT_ALL;
	} else {
		return Xchat::EAT_NONE;
	}
}

#if there's no response to /WHOIS, stop waiting after a while
sub reset_timer
{
	undef $info{nick};
	return Xchat::REMOVE;
}

Xchat::register("IP2C", VERSION, "IP2C");
Xchat::hook_server("PRIVMSG", \&server_hook);
Xchat::hook_print("WhoIs Special",\&whois_special);
Xchat::hook_print("WhoIs Name Line",\&whois_name_line);
Xchat::hook_print("WhoIs End",\&whois_end);

for my $w ("Authenticated","Away Line","Channel/Oper Line","Identified","Idle Line","Idle Line with Signon",
           "Real Host",#TODO: this is probably useful, find a server that sends it
           "Server Line") {
	Xchat::hook_print("WhoIs $w",\&whois_suppress);
}

Xchat::hook_command('ip2c', \&command_hook, {help_text => 'Convert IP to country'});

Xchat::print "\002IP2C\002 version ".VERSION;
