# File: BanTracker.pm
# Author: pragma_
#
# Purpose: Populates and maintains channel banlists by checking mode +b on
# joining channels and by tracking modes +b and -b in channels.
#
# Does NOT do banning or unbanning.

package PBot::BanTracker;

use warnings;
use strict;

use Time::HiRes qw/gettimeofday/;
use Time::Duration;
use Data::Dumper;
use Carp ();

sub new {
  if(ref($_[1]) eq 'HASH') {
    Carp::croak("Options to BanTracker should be key/value pairs, not hash reference");
  }

  my ($class, %conf) = @_;

  my $self = bless {}, $class;
  $self->initialize(%conf);
  return $self;
}

sub initialize {
  my ($self, %conf) = @_;

  $self->{pbot}    = delete $conf{pbot} // Carp::croak("Missing pbot reference to BanTracker");
  $self->{banlist} = {};

  $self->{pbot}->{registry}->add_default('text', 'bantracker', 'chanserv_ban_timeout', '604800');
  $self->{pbot}->{registry}->add_default('text', 'bantracker', 'debug',                '0');

  $self->{pbot}->{commands}->register(sub { $self->dumpbans(@_) }, "dumpbans", 60);

  $self->{pbot}->{event_dispatcher}->register_handler('irc.endofnames', sub { $self->get_banlist(@_) });
  $self->{pbot}->{event_dispatcher}->register_handler('irc.banlist',    sub { $self->on_banlist_entry(@_) });
  $self->{pbot}->{event_dispatcher}->register_handler('irc.quietlist',  sub { $self->on_quietlist_entry(@_) });
}

sub dumpbans {
  my ($self, $from, $nick, $user, $host, $arguments) = @_;

  my $bans = Dumper($self->{banlist});
  return $bans;
}

sub get_banlist {
  my ($self, $event_type, $event) = @_;
  my $channel = lc $event->{event}->{args}[1];

  delete $self->{banlist}->{$channel};

  $self->{pbot}->{logger}->log("Retrieving banlist for $channel.\n");
  $event->{conn}->sl("mode $channel +b");
  $event->{conn}->sl("mode $channel +q");
  return 0;
}

sub on_banlist_entry {
  my ($self, $event_type, $event) = @_;
  my $channel   = lc $event->{event}->{args}[1];
  my $target    = lc $event->{event}->{args}[2];
  my $source    = lc $event->{event}->{args}[3];
  my $timestamp =    $event->{event}->{args}[4];

  my $ago = ago(gettimeofday - $timestamp);

  $self->{pbot}->{logger}->log("ban-tracker: [banlist entry] $channel: $target banned by $source $ago.\n");
  $self->{banlist}->{$channel}->{'+b'}->{$target} = [ $source, $timestamp ];
  return 0;
}

sub on_quietlist_entry {
  my ($self, $event_type, $event) = @_;
  my $channel   = lc $event->{event}->{args}[1];
  my $target    = lc $event->{event}->{args}[3];
  my $source    = lc $event->{event}->{args}[4];
  my $timestamp =    $event->{event}->{args}[5];

  my $ago = ago(gettimeofday - $timestamp);

  $self->{pbot}->{logger}->log("ban-tracker: [quietlist entry] $channel: $target quieted by $source $ago.\n");
  $self->{banlist}->{$channel}->{'+q'}->{$target} = [ $source, $timestamp ];
  return 0;
}

sub get_baninfo {
  my ($self, $mask, $channel, $account) = @_;
  my ($bans, $ban_account);

  $account = lc $account if defined $account;

  if ($self->{pbot}->{registry}->get_value('bantracker', 'debug')) {
    $self->{pbot}->{logger}->log("[get-baninfo] Getting baninfo for $mask in $channel using account " . (defined $account ? $account : "[undefined]") . "\n");
  }

  foreach my $mode (keys %{ $self->{banlist}{$channel} }) {
    foreach my $banmask (keys %{ $self->{banlist}{$channel}{$mode} }) {
      if($banmask =~ m/^\$a:(.*)/) {
        $ban_account = lc $1;
      } else {
        $ban_account = "";
      }

      my $banmask_key = $banmask;
      $banmask = quotemeta $banmask;
      $banmask =~ s/\\\*/.*?/g;
      $banmask =~ s/\\\?/./g;

      if((defined $account and $account eq $ban_account) or $mask =~ m/^$banmask$/i) {
        if(not defined $bans) {
          $bans = [];
        }

        my $baninfo = {};
        $baninfo->{banmask} = $banmask_key;
        $baninfo->{channel} = $channel;
        $baninfo->{owner} = $self->{banlist}{$channel}{$mode}{$banmask_key}[0];
        $baninfo->{when} = $self->{banlist}{$channel}{$mode}{$banmask_key}[1];
        $baninfo->{type} = $mode;
        $self->{pbot}->{logger}->log("get-baninfo: dump: " . Dumper($baninfo) . "\n");

        push @$bans, $baninfo;
      }
    }
  }

  return $bans;
}

sub track_mode {
  my $self = shift;
  my ($source, $mode, $target, $channel) = @_;

  if($mode eq "+b" or $mode eq "+q") {
    $self->{pbot}->{logger}->log("ban-tracker: $target " . ($mode eq '+b' ? 'banned' : 'quieted') . " by $source in $channel.\n");
    $self->{banlist}->{$channel}->{$mode}->{$target} = [ $source, gettimeofday ];
    $self->{pbot}->{antiflood}->devalidate_accounts($target, $channel);
  }
  elsif($mode eq "-b" or $mode eq "-q") {
    $self->{pbot}->{logger}->log("ban-tracker: $target " . ($mode eq '-b' ? 'unbanned' : 'unquieted') . " by $source in $channel.\n");
    delete $self->{banlist}->{$channel}->{$mode eq "-b" ? "+b" : "+q"}->{$target};

    if($mode eq "-b") {
      if($self->{pbot}->{chanops}->{unban_timeout}->find_index($channel, $target)) {
        $self->{pbot}->{chanops}->{unban_timeout}->remove($channel, $target);
      } elsif($self->{pbot}->{chanops}->{unban_timeout}->find_index($channel, "$target\$##stop_join_flood")) {
        # freenode strips channel forwards from unban result if no ban exists with a channel forward
        $self->{pbot}->{chanops}->{unban_timeout}->remove($channel, "$target\$##stop_join_flood");
      }
    }
  } else {
    $self->{pbot}->{logger}->log("BanTracker: Unknown mode '$mode'\n");
  }
}

1;
