The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Convos::Core::Connection;

=head1 NAME

Convos::Core::Connection - Represents a connection to an IRC server

=head1 SYNOPSIS

  use Convos::Core::Connection;

  $c = Convos::Core::Connection->new(
         name => 'magnet',
         login => 'username',
         redis => Mojo::Redis->new,
       );

  $c->connect;

  Mojo::IOLoop->start;

=head1 DESCRIPTION

This module use L<Mojo::IRC> to  up a connection to an IRC server. The
attributes used to do so is figured out from a redis server.

There are quite a few L<EVENTS|Mojo::IRC/EVENTS> that this module use:

=over 4

=item * L</add_message> events

L<Mojo::IRC/privmsg>.

=item * L</add_server_message> events

L<Mojo::IRC/rpl_yourhost>, L<Mojo::IRC/rpl_motdstart>, L<Mojo::IRC/rpl_motd>,
L<Mojo::IRC/rpl_endofmotd>, L<Mojo::IRC/rpl_welcome> and L<Mojo::IRC/error>.

=item * Other events

L</irc_rpl_welcome>, L</irc_rpl_myinfo>, L</irc_join>, L</irc_part>,
L</irc_rpl_namreply>, L</err_nosuchchannel>, L</err_notonchannel>, L</err_nosuchnick>
L</err_bannedfromchan>, l</irc_error> and L</irc_quit>.

=back

=cut

use Mojo::Base 'Mojo::EventEmitter';
use Mojo::IRC;
use Mojo::JSON 'j';
no warnings 'utf8';
use IRC::Utils;
use Parse::IRC   ();
use Scalar::Util ();
use Time::HiRes 'time';
use Convos::Core::Util qw( as_id id_as );
use Sys::Hostname ();
use constant CHANNEL_LIST_CACHE_TIMEOUT => 3600;    # TODO: Figure out how long to cache channel list
use constant DEBUG => $ENV{CONVOS_DEBUG} ? 1 : 0;

=head1 ATTRIBUTES

=head2 name

Name of the connection. Example: "freenode", "magnet" or "efnet".

=head2 log

Holds a L<Mojo::Log> object.

=head2 login

The username of the owner.

=head2 redis

Holds a L<Mojo::Redis> object.

=cut

has name  => '';
has log   => sub { Mojo::Log->new };
has login => 0;
has redis => sub { die 'redis connection required in constructor' };

my @ADD_MESSAGE_EVENTS        = qw( irc_privmsg ctcp_action irc_notice );
my @ADD_SERVER_MESSAGE_EVENTS = qw(
  irc_rpl_yourhost irc_rpl_motdstart irc_rpl_motd irc_rpl_endofmotd
  irc_rpl_welcome rpl_luserclient
);
my @OTHER_EVENTS = qw(
  irc_rpl_welcome irc_rpl_myinfo irc_join irc_nick irc_part irc_479
  irc_rpl_whoisuser irc_rpl_whoisidle irc_rpl_whoischannels irc_rpl_endofwhois
  irc_rpl_topic irc_topic
  irc_rpl_topicwhotime irc_rpl_notopic err_nosuchchannel err_nosuchnick
  err_notonchannel err_bannedfromchan irc_rpl_list
  irc_rpl_listend irc_mode irc_quit irc_kick irc_error
  irc_rpl_namreply irc_rpl_endofnames err_nicknameinuse
);

has _irc => sub {
  my $self = shift;
  my $irc = Mojo::IRC->new(debug_key => join ':', $self->login, $self->name);

  $irc->parser(Parse::IRC->new(ctcp => 1));

  Scalar::Util::weaken($self);
  $irc->register_default_event_handlers;
  $irc->on(close => sub { $self->_irc_close });
  $irc->on(error => sub { $self->_irc_error($_[1]) });

  for my $event (@ADD_MESSAGE_EVENTS) {
    $irc->on($event => sub { $self->add_message($_[1]) });
  }
  for my $event (@ADD_SERVER_MESSAGE_EVENTS) {
    $irc->on($event => sub { $self->add_server_message($_[1]) });
  }
  for my $event (@OTHER_EVENTS) {
    $irc->on($event => sub { $_[1]->{handled}++ or $self->$event($_[1]) });
  }

  $irc;
};

sub _irc_close {
  my $self = shift;
  my $name = $self->_irc->name;

  $self->_state('disconnected');

  if ($self->{stop}) {
    $self->_publish_and_save(server_message => {status => 200, message => 'Disconnected.'});
    return;
  }

  $self->_publish_and_save(server_message => {status => 500, message => "Disconnected from $name."});
  $self->_reconnect;
}

sub _irc_error {
  my ($self, $error) = @_;
  my $name = $self->_irc->name;

  $self->{stop} and return $self->_state('disconnected');
  $self->_state('disconnected');
  $self->_publish_and_save(server_message => {status => 500, message => "Connection to $name failed: $error"});
  $self->_reconnect;
}

=head1 METHODS

=head2 new

Checks for mandatory attributes: L</login> and L</name>.

=cut

sub new {
  my $self = shift->SUPER::new(@_);

  $self->{login} or die "login is required";
  $self->{name}  or die "name is required";
  $self->{conversation_path} = "user:$self->{login}:conversations";
  $self->{path}              = "user:$self->{login}:connection:$self->{name}";
  $self->{state}             = 'disconnected';
  $self;
}

=head2 connect

  $self = $self->connect;

This method will create a new L<Mojo::IRC> object with attribute data from
L</redis>. The values fetched from the backend is identified by L</name> and
L</login>. This method then call L<Mojo::IRC/connect> after the object is set
up.

Attributes fetched from backend: nick, user, host and channels. The latter
is set in L</channels> and used by L</irc_rpl_welcome>.

=cut

sub connect {
  my ($self) = @_;
  my $irc = $self->_irc;

  Scalar::Util::weaken($self);
  $self->{core_connect_timer} = 0;
  $self->{keepnick_tid} ||= $irc->ioloop->recurring(60 => sub { $self->_steal_nick });
  $self->_subscribe;

  $self->redis->execute(
    [hgetall => $self->{path}],
    [get     => 'convos:frontend:url'],
    sub {
      my ($redis, $args, $url) = @_;
      $self->redis->hset($self->{path} => tls => $self->{disable_tls} ? 0 : 1);
      $irc->name($url || 'Convos');
      $irc->nick($args->{nick} || $self->login);
      $irc->pass($args->{password}) if $args->{password};
      $irc->server($args->{server} || $args->{host});
      $irc->tls($self->{disable_tls} ? undef : {});
      $irc->user($args->{username} || $self->login);
      $irc->connect(
        sub {
          my ($irc, $error) = @_;
          $error and return $self->_connect_failed($error);
          $self->_publish_and_save(server_message => {status => 200, message => "Connected to IRC server"});
          $self->_state('connected');
        },
      );
    },
  );

  $self;
}

sub _state {
  my ($self, $state) = @_;

  $self->{state} = $state;
  $self->redis->hset($self->{path}, state => $state);
  $self;
}

sub _steal_nick {
  my $self = shift;

  # We will try to "steal" the nich we really want every 60 second
  Mojo::IOLoop->delay(
    sub {
      my ($delay) = @_;
      $self->redis->hget($self->{path}, 'nick', $delay->begin);
    },
    sub {
      my ($delay, $nick) = @_;
      $self->_irc->write(NICK => $nick) if $nick and $self->_irc->nick ne $nick;
    }
  );
}

sub _subscribe {
  my $self = shift;
  my $irc  = $self->_irc;

  Scalar::Util::weaken($self);
  $self->{messages} = $self->redis->subscribe("convos:user:@{[$self->login]}:@{[$self->name]}");
  $self->{messages}->on(
    error => sub {
      my ($sub, $error) = @_;
      $self->log->warn("[$self->{path}] Re-subcribing to messages to @{[$irc->name]}. ($error)");
      $self->_subscribe;
    },
  );
  $self->{messages}->on(
    message => sub {
      my ($sub, $raw_message) = @_;
      my ($uuid, $message);

      $raw_message =~ s/(\S+)\s//;
      $uuid        = $1;
      $raw_message = sprintf ':%s %s', $irc->nick, $raw_message;
      $message     = Parse::IRC::parse_irc($raw_message);

      unless (ref $message) {
        $self->_publish_and_save(
          server_message => {status => 400, message => "Unable to parse: $raw_message", uuid => $uuid});
        return;
      }

      $message->{uuid} = $uuid;

      $irc->write(
        $raw_message,
        sub {
          my ($irc, $error) = @_;

          if ($error) {
            $self->_publish_and_save(server_message =>
                {status => 500, message => "Could not send message to @{[$irc->name]}: $error", uuid => $uuid});
          }
          elsif ($message->{command} eq 'PRIVMSG') {
            $self->add_message($message);
          }
          elsif (my $method = $self->can('cmd_' . lc $message->{command})) {
            $self->$method($message);
          }
        }
      );
    }
  );

  $self;
}

=head2 channels_from_conversations

  @channels = $self->channels_from_conversations(\@conversations);

This method returns an array ref of channels based on the conversations
input. It will use L</name> to filter out the right list.

=cut

sub channels_from_conversations {
  my ($self, $conversations) = @_;

  map { lc $_->[1] } grep { $_->[0] eq $self->name and $_->[1] =~ /^[#&]/ } map { [id_as $_ ] } @{$conversations || []};
}

=head2 add_server_message

  $self->add_server_message(\%message);

Will look at L<%message> and add it to the database as a server message
if it looks like one. Returns true if the message was added to redis.

=cut

sub add_server_message {
  my ($self, $message) = @_;
  my $params = $message->{params};
  my $data = {status => 200};

  shift @$params;    # I think this removes our own nick... Not quite sure though
  $data->{message} = join ' ', @$params;
  $message->{command} ||= '';

  $self->_state('connected');
  $self->_publish_and_save(server_message => $data);
}

=head2 add_message

  $self->add_message(\%message);

Will add a private message to the database.

=cut

sub add_message {
  my ($self, $message) = @_;
  my $current_nick       = $self->_irc->nick;
  my $is_private_message = $message->{params}[0] eq $current_nick;
  my $data = {highlight => 0, message => $message->{params}[1], timestamp => time, uuid => $message->{uuid},};

  @$data{qw( nick user host )} = IRC::Utils::parse_user($message->{prefix}) if $message->{prefix};
  $data->{target} = lc($is_private_message ? $data->{nick} : $message->{params}[0]);
  $data->{host} ||= 'localhost';

  if ($data->{nick}) {
    if ($data->{nick} eq $current_nick) {
      $data->{user} ||= $self->_irc->user;
    }
    elsif ($is_private_message or $data->{message} =~ /\b$current_nick\b/) {
      $self->_add_conversation($data->{target}) if $is_private_message and $data->{user};
      $data->{highlight} = 1;
    }
  }

  if (!$data->{user}) {    # server notice/message
    return $self->add_server_message($message);
  }

  # need to take care of when the current user also writes /me...
  # this is not yet tested, since i have no time right now :(
  if ($data->{message} =~ s/\x{1}ACTION (.*)\x{1}/$1/) {
    $message->{command} = "CTCP_ACTION";
  }

  $self->_publish_and_save($message->{command} eq 'CTCP_ACTION' ? 'action_message' : 'message', $data);
}

sub _add_conversation {
  my ($self, $target) = @_;
  my $name = as_id $self->name, $target;

  Mojo::IOLoop->delay(
    sub {
      my ($delay) = @_;
      $self->redis->zincrby($self->{conversation_path}, 0, $name, $delay->begin);
    },
    sub {
      my ($delay, $part_of_conversation_list) = @_;
      $part_of_conversation_list and return;
      $self->redis->zrevrange($self->{conversation_path}, 0, 0, 'WITHSCORES', $delay->begin);
    },
    sub {
      my ($delay, $score) = @_;
      $self->redis->zadd($self->{conversation_path}, $score->[1] - 0.0001, $name, $delay->begin);
    },
    sub {
      my ($delay) = @_;
      $self->_publish(add_conversation => {target => $target});
    },
  );
}

=head2 disconnect

Will disconnect from the L</irc> server.

=cut

sub disconnect {
  my ($self, $cb) = @_;
  $self->{stop} = 1;
  $self->_irc->disconnect($cb || sub { });
}

=head1 EVENT HANDLERS

=head2 irc_rpl_welcome

Example message:

:Zurich.CH.EU.Undernet.Org 001 somenick :Welcome to the UnderNet IRC Network, somenick

=cut

sub irc_rpl_welcome {
  my ($self, $message) = @_;

  $self->{attempts} = 0;

  Scalar::Util::weaken($self);
  $self->redis->zrange(
    $self->{conversation_path},
    0, -1,
    sub {
      for my $channel ($self->channels_from_conversations($_[1])) {
        $self->redis->hget(
          "$self->{path}:$channel",
          key => sub {
            $_[1] ? $self->_irc->write(JOIN => $channel, $_[1]) : $self->_irc->write(JOIN => $channel);
          }
        );
      }
    }
  );
}

=head2 irc_rpl_endofwhois

Use data from L</irc_rpl_whoisidle>, L</irc_rpl_whoisuser> and
L</irc_rpl_whoischannels>.

=cut

sub irc_rpl_endofwhois {
  my ($self, $message) = @_;
  my $nick = $message->{params}[1];
  my $whois = delete $self->{whois}{$nick} || {};

  $whois->{channels} ||= [];
  $whois->{idle}     ||= 0;
  $whois->{realname} ||= '';
  $whois->{user}     ||= '';
  $whois->{nick} = $nick;
  $self->_publish(whois => $whois) if $whois->{host};
}

=head2 irc_rpl_whoisidle

Store idle info internally. See L</irc_rpl_endofwhois>.

=cut

sub irc_rpl_whoisidle {
  my ($self, $message) = @_;
  my $nick = $message->{params}[1];

  $self->{whois}{$nick}{idle} = $message->{params}[2] || 0;
}

=head2 irc_rpl_whoisuser

Store user info internally. See L</irc_rpl_endofwhois>.

=cut

sub irc_rpl_whoisuser {
  my ($self, $message) = @_;
  my $params = $message->{params};
  my $nick   = $params->[1];

  $self->{whois}{$nick}{host}     = $params->[3];
  $self->{whois}{$nick}{realname} = $params->[5];
  $self->{whois}{$nick}{user}     = $params->[2];
}

=head2 irc_rpl_whoischannels

Reply with user channels

=cut

sub irc_rpl_whoischannels {
  my ($self, $message) = @_;
  my $nick = $message->{params}[1];

  push @{$self->{whois}{$nick}{channels}}, split ' ', $message->{params}[2] || '';
}

=head2 irc_rpl_notopic

  :server 331 nick #channel :No topic is set.

=cut

sub irc_rpl_notopic {
  my ($self, $message) = @_;
  my $target = lc $message->{params}[1];

  $self->redis->hset("$self->{path}:$target", topic => '');
  $self->_publish(topic => {topic => '', target => $target});
}

=head2 irc_rpl_topic

Reply with topic

=cut

sub irc_rpl_topic {
  my ($self, $message) = @_;
  my $target = lc $message->{params}[1];
  my $topic  = $message->{params}[2];

  $self->redis->hset("$self->{path}:$target", topic => $topic);
  $self->_publish(topic => {topic => $topic, target => $target});
}

=head2 irc_topic

  :nick!~user@hostname TOPIC #channel :some topic

=cut

sub irc_topic {
  my ($self, $message) = @_;
  my $target = lc $message->{params}[0];
  my $topic  = $message->{params}[1];

  $self->redis->hset("$self->{path}:$target", topic => $topic);
  $self->_publish(topic => {topic => $topic, target => $target});
}

=head2 irc_rpl_topicwhotime

Reply with who and when for topic change

=cut

sub irc_rpl_topicwhotime {
  my ($self, $message) = @_;

  $self->_publish(topic_by =>
      {timestamp => $message->{params}[3], nick => $message->{params}[2], target => lc $message->{params}[1],});
}

=head2 irc_rpl_myinfo

Example message:

:Tampa.FL.US.Undernet.org 004 somenick Tampa.FL.US.Undernet.org u2.10.12.14 dioswkgx biklmnopstvrDR bklov

=cut

sub irc_rpl_myinfo {
  my ($self, $message) = @_;
  my @keys = qw/ current_nick real_host version available_user_modes available_channel_modes /;
  my $i    = 0;

  $self->redis->hmset($self->{path}, map { $_, $message->{params}[$i++] // '' } @keys);
}

=head2 irc_479

Invalid channel name.

=cut

sub irc_479 {
  my ($self, $message) = @_;

  # params => [ 'nickname', '1', 'Illegal channel name' ],
  $self->_publish(server_message => {status => 400, message => $message->{params}[2] || 'Illegal channel name'});
}

=head2 irc_join

See L<Mojo::IRC/irc_join>.

=cut

sub irc_join {
  my ($self, $message) = @_;
  my ($nick, $user, $host) = IRC::Utils::parse_user($message->{prefix});
  my $channel = lc $message->{params}[0];

  if ($nick eq $self->_irc->nick) {
    $self->redis->hset("$self->{path}:$channel", topic => '');
    $self->redis->hset("convos:host2convos" => $host => 'loopback');
    $self->_add_conversation($channel);
  }
  else {
    $self->_publish(nick_joined => {nick => $nick, target => $channel});
  }
}

=head2 irc_nick

  :old_nick!~username@1.2.3.4 NICK :new_nick

=cut

sub irc_nick {
  my ($self, $message) = @_;
  my ($old_nick) = IRC::Utils::parse_user($message->{prefix});
  my $new_nick = $message->{params}[0];

  if ($new_nick eq $self->_irc->nick) {
    delete $self->{supress}{err_nicknameinuse};
    $self->redis->hset($self->{path}, current_nick => $new_nick);
  }

  $self->_publish(nick_change => {old_nick => $old_nick, new_nick => $new_nick});
}

=head2 irc_quit

  {
    params => [ 'Quit: leaving' ],
    raw_line => ':nick!~user@localhost QUIT :Quit: leaving',
    command => 'QUIT',
    prefix => 'nick!~user@localhost'
  };

=cut

sub irc_quit {
  my ($self, $message) = @_;
  my ($nick) = IRC::Utils::parse_user($message->{prefix});

  Scalar::Util::weaken($self);
  $self->_publish(nick_quit => {nick => $nick, message => $message->{params}[0]});
}

=head2 irc_kick

  'raw_line' => ':testing!~marcus@home.means.no KICK #testmore :marcus_',
  'params' => [ '#testmore', 'marcus_' ],
  'command' => 'KICK',
  'handled' => 1,
  'prefix' => 'testing!~marcus@40.101.45.31.customer.cdi.no'

=cut

sub irc_kick {
  my ($self, $message) = @_;
  my ($by)    = IRC::Utils::parse_user($message->{prefix});
  my $channel = lc $message->{params}[0];
  my $nick    = $message->{params}[1];

  if ($nick eq $self->_irc->nick) {
    my $name = as_id $self->name, $channel;
    $self->redis->zrem($self->{conversation_path}, $name, sub { });
  }

  $self->_publish(nick_kicked => {by => $by, nick => $nick, target => $channel});
}

=head2 irc_part

=cut

sub irc_part {
  my ($self, $message) = @_;
  my ($nick) = IRC::Utils::parse_user($message->{prefix});
  my $channel = lc $message->{params}[0];

  Scalar::Util::weaken($self);
  if ($nick eq $self->_irc->nick) {
    my $name = as_id $self->name, $channel;

    $self->redis->zrem(
      $self->{conversation_path},
      $name,
      sub {
        $self->_publish(remove_conversation => {target => $channel});
      }
    );
  }
  else {
    $self->_publish(nick_parted => {nick => $nick, target => $channel});
  }
}

=head2 err_bannedfromchan

:electret.shadowcat.co.uk 474 nick #channel :Cannot join channel (+b)

=cut

sub err_bannedfromchan {
  my ($self, $message) = @_;
  my $channel = lc $message->{params}[1];
  my $name = as_id $self->name, $channel;

  $self->_publish_and_save(server_message => {status => 401, message => $message->{params}[2]});

  Scalar::Util::weaken($self);
  $self->redis->zrem(
    $self->{conversation_path},
    $name,
    sub {
      $self->_publish(remove_conversation => {target => $channel});
    }
  );
}

=head2 err_nicknameinuse

=cut

sub err_nicknameinuse {
  my ($self, $message) = @_;

  if ($self->{supress}{err_nicknameinuse}++) {
    return;
  }

  $self->_publish(server_message => {status => 500, message => $message->{params}[2],});
}

=head2 err_nosuchchannel

:astral.shadowcat.co.uk 403 nick #channel :No such channel

=cut

sub err_nosuchchannel {
  my ($self, $message) = @_;
  my $channel = lc $message->{params}[1];
  my $name = as_id $self->name, $channel;

  $self->_publish(server_message => {status => 400, message => qq(No such channel "$channel")});

  if ($channel =~ /^[#&]/) {
    Scalar::Util::weaken($self);
    $self->redis->zrem(
      $self->{conversation_path},
      $name,
      sub {
        $self->_publish(remove_conversation => {target => $channel});
      }
    );
  }
}

=head2 err_nosuchnick

  :electret.shadowcat.co.uk 442 sender nick :No such nick

=cut

sub err_nosuchnick {
  my ($self, $message) = @_;

  $self->_publish(err_nosuchnick => {nick => $message->{params}[1]});
}

=head2 err_notonchannel

:electret.shadowcat.co.uk 442 nick #channel :You're not on that channel

=cut

sub err_notonchannel {
  shift->err_nosuchchannel(@_);
}

=head2 irc_rpl_endofnames

Example message:

  :magnet.llarian.net 366 somenick #channel :End of /NAMES list.

=cut

sub irc_rpl_endofnames {
  my ($self, $message) = @_;
  my $channel = lc $message->{params}[1] or return;
  my $nicks = delete $self->{nicks}{$channel} || [];

  $self->_publish(rpl_namreply => {nicks => $nicks, target => $channel});
}

=head2 irc_rpl_namreply

Example message:

  :Budapest.Hu.Eu.Undernet.org 353 somenick = #channel :somenick Indig0 Wildblue @HTML @CSS @Luch1an @Steaua_ Indig0_ Pilum @fade

=cut

sub irc_rpl_namreply {
  my ($self, $message) = @_;
  my $channel = lc $message->{params}[2] or return;
  my $nicks = $self->{nicks}{$channel} ||= [];

  for my $nick (sort { lc $a cmp lc $b } split /\s+/, $message->{params}[3]) {    # 3 = "+nick0 @nick1 nick2"
    my $mode = $nick =~ s/^([@~+*])// ? $1 : '';
    push @$nicks, {nick => $nick, mode => $mode};
  }
}

=head2 irc_rpl_list

:servername 322 somenick #channel 10 :[+n] some topic

=cut

sub irc_rpl_list {
  my ($self, $message) = @_;
  my $network = $self->name;
  my $name    = $message->{params}[1];
  my %info    = (name => $name, visible => $message->{params}[2], title => $message->{params}[3] // '');

  $self->_publish(channel_info => {name => $name, network => $network, info => \%info});
  $self->redis->hset("convos:irc:$network:channels", $name => j \%info) if $self->{save_channels};
}

=head2 irc_rpl_listend

:servername 323 somenick :End of /LIST

=cut

sub irc_rpl_listend {
  my ($self, $message) = @_;
  my $network = $self->name;

  $self->redis->expire("convos:irc:$network:channels", CHANNEL_LIST_CACHE_TIMEOUT) if delete $self->{save_channels};
}

=head2 irc_mode

  :nick!user@host MODE #channel +o othernick
  :nick!user@host MODE yournick +i

=cut

sub irc_mode {
  my ($self, $message) = @_;
  my $target = lc shift @{$message->{params}};
  my $mode   = shift @{$message->{params}};

  if ($target eq lc $self->_irc->nick) {
    $self->_publish(server_message =>
        {status => 200, target => $self->name, message => "You are connected to @{[$self->name]} with mode $mode"});
  }
  else {
    $self->_publish(mode => {target => $target, mode => $mode, args => join(' ', @{$message->{params}})});
  }
}

=head2 irc_error

Example message:

ERROR :Closing Link: somenick by Tampa.FL.US.Undernet.org (Sorry, your connection class is full - try again later or try another server)

=cut

sub irc_error {
  my ($self, $message) = @_;

  # Server dislikes us, we'll back off more
  $self->{attempts} += 10;
  $self->_publish_and_save(server_message => {status => 500, message => join(' ', @{$message->{params}})});
}

=head2 cmd_nick

Handle nick commands from user. Change nick and set new nick in redis.

=cut

sub cmd_nick {
  my ($self, $message) = @_;
  my $new_nick = $message->{params}[0];

  if ($new_nick =~ /^[\w-]+$/) {
    $self->redis->hset($self->{path}, nick => $new_nick);
    $self->_publish(server_message => {status => 200, message => 'Set nick to ' . $new_nick});
  }
  else {
    $self->_publish(server_message => {status => 400, message => 'Invalid nick'});
  }
}

=head2 cmd_join

Store keys on channel join.

=cut

sub cmd_join {
  my ($self, $message) = @_;

  my $channel = $message->{params}[0];
  if (my $key = $message->{params}[1]) {
    $self->redis->hset("$self->{path}:$channel", key => $key);
  }
}

=head2 cmd_list

=cut

sub cmd_list {
  my ($self, $message) = @_;
  my $network = $self->name;

  $self->{channels} = {};

  if (my $filter = $message->{params}[0] || '') {
    $self->{channels}{lc($_)} = {name => $_, topic => '', not_found => 1} for split /,/, $filter;
  }
  else {
    $self->redis->del("convos:irc:$network:channels");
    $self->{save_channels} = 1;
  }
}

sub _connect_failed {
  my ($self, $error) = @_;
  my $server = $self->_irc->server;

  # SSL connect attempt failed with unknown error
  # error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol
  if ($error =~ /SSL\d*_GET_SERVER_HELLO/) {
    $self->_state('reconnecting');
    $self->_publish_and_save(
      server_message => {status => 400, message => "This IRC network ($server) does not support SSL/TLS."});
    $self->{disable_tls}        = 1;
    $self->{core_connect_timer} = 1;
  }
  else {
    $self->_state('disconnected');
    $self->_publish_and_save(server_message => {status => 500, message => "Could not connect to $server: $error"});
    $self->_reconnect;
  }
}

sub _publish {
  my ($self, $event, $data) = @_;
  my $login = $self->login;
  my $name  = $self->name;
  my $message;

  local $data->{state} = $self->{state};

  $data->{event}   = $event;
  $data->{network} = $name;
  $data->{timestamp} ||= time;
  $data->{uuid} ||= Mojo::Util::md5_sum($data->{timestamp} . $$);    # not really an uuid
  $message = j $data;

  if ($event eq 'server_message' and $data->{status} != 200) {
    $self->log->warn("[$login:$name] $data->{message}");
  }

  $self->redis->publish("convos:user:$login:out", $message);
  $message;
}

sub _publish_and_save {
  my ($self, $event, $data) = @_;
  my $login = $self->login;
  my $message = $self->_publish($event, $data);

  if ($data->{highlight}) {

    # Ooops! This must be broken: We're clearing the notification by index in
    # Client.pm, but the index we're clearing does not have to be the index in
    # the list. The bug should appear if we use an old ?notification=42 link
    # and in the meanwhile we have added more notifications..?
    $self->redis->lpush("user:$login:notifications", $message);
  }

  if ($data->{target}) {
    $self->redis->zadd("$self->{path}:$data->{target}:msg", $data->{timestamp}, $message);
  }
  else {
    $self->redis->zadd("$self->{path}:msg", $data->{timestamp}, $message);
  }

  $self->emit(save => $data);
}

sub _reconnect {
  my $self = shift;
  $self->{attempts}++;
  $self->{core_connect_timer} = 30 * $self->{attempts};    # CONNECT_INTERVAL * 30 = 60 seconds
}

sub DESTROY {
  warn "DESTROY $_[0]->{path}\n" if DEBUG;
  my $self         = shift;
  my $ioloop       = $self->{_irc}{ioloop} or return;
  my $keepnick_tid = $self->{keepnick_tid} or return;
  $ioloop->remove($keepnick_tid);
}

=head1 COPYRIGHT

See L<Convos>.

=head1 AUTHOR

Jan Henning Thorsen

Marcus Ramberg

=cut

1;