/server/ChatNoir/Client.pm
package ChatNoir::Client;
use Class::Accessor 'moose-like';
use AnyEvent::XMPP::IM::Connection;

use ChatNoir::Util qw/pretty_presence short_jid/;

use strict;
use v5.10;

has app        => ( is => 'ro' );
has controller => ( is => 'ro' );
has xmpp       => ( is => 'ro' );
has jid        => ( is => 'ro' );
has password   => ( is => 'ro' );
has active     => ( is => 'ro' );

sub attach {
    my ($self, %params) = @_;

    if ($self->controller) {
        $self->controller->finish(1000 => "Reconnected");
    }
    $self->{controller} = $params{controller};

    $self->attach_controller_events;
}

sub attach_controller_events {
    my ($self) = @_;

    $self->controller->tx->on(json => sub {
        my ($tx, $msg) = @_;
        given ($msg->{type}) {
            when ('login') {
                my $password = $msg->{password};

                if (!defined $password) {
                    $self->controller->finish(1015 => "Login must have password\n");
                    delete $self->{controller};
                    return;
                }

                if ($self->xmpp && $self->xmpp->is_connected) {
                    # If we're reattaching, check that the new password matches the original
                    if ($password ne $self->password) {
                        $self->ws_send('login-failed', reason => 'unauthorized');
                        return;
                    }
                    $self->{active} = 1;
                    $self->ws_send('login-successful');
                    $self->app->log->debug("reattaching " . $self->jid);
                } else {
                    # Otherwise, attempt a new connection with the given password
                    $self->app->log->debug("Creating new XMPP connection for " . $self->jid);
                    $self->{password} = $password;
                    $self->{xmpp} = AnyEvent::XMPP::IM::Connection->new(
                        jid => $self->jid,
                        password => $password,
                    );
                    $self->attach_xmpp_events;
                    $self->xmpp->connect;
                }
            }
            when ('logout') {
                $self->active || return;
                $self->guard_connected || return;
                $self->xmpp->disconnect;
                delete $self->{xmpp};
                $self->ws_send('login-failed', reason => 'user requested');
                $self->app->log->debug($self->jid . " logged out");
            }
            when ('message') {
                $self->active || return;
                $self->guard_connected || return;
                AnyEvent::XMPP::IM::Message->new(
                    from => $self->jid,
                    to => $msg->{to},
                    body => $msg->{body},
                )->send($self->xmpp);
            }
            when ('get-roster') {
                $self->active || return;
                $self->guard_connected || return;
                $self->ws_send_roster;
            }
            when (undef) {
                $self->app->log->debug("Empty message?");
            }
            default {
                $self->app->log->debug("Unknown message type $msg->{type}");
            }
        }
    });

    $self->controller->tx->on(finish => sub {
        delete $self->{controller};
        $self->{active} = undef;
        $self->app->log->debug($self->jid . " detached");
    });
}

sub attach_xmpp_events {
    my ($self) = @_;

    $self->xmpp->reg_cb(
        session_ready => sub {
            $self->ws_send('login-successful');
            $self->{active} = 1;
            $self->app->log->debug("Session ready for " . $self->xmpp->jid);
        },
        session_error => sub {
            my ($err) = @_;
            $self->ws_send('login-failed', reason => 'error', error => $err->string);
            $self->{active} = undef;
            $self->app->log->error("Session error: " . $err->string);
        },
        sasl_error => sub {
            my ($connection, $err) = @_;
            $self->{active} = undef;
            $self->app->log->error($err->string);
            if ($err->string =~ /not-authorized/) {
                $self->ws_send('login-failed', reason => 'unauthorized');
            }
        },
        roster_update => sub {
            my ($connection, $roster, $contacts) = @_;
        },
        presence_update => sub {
            my ($connection, $roster, $contact, $old_presence, $new_presence) = @_;
            $self->ws_send('presence',
                jid => short_jid($new_presence->jid),
                status => pretty_presence($new_presence),
                priority => $new_presence->priority,
            );
        },
        message => sub {
            my ($connection, $msg) = @_;

            $self->ws_send('message',
                from => short_jid($msg->from),
                body => $msg->body,
            );
        }
    );
}

sub ws_send {
    my ($self, $type, %params) = @_;

    if ($self->controller) {
        $self->controller->send({json => {
            type => $type,
            %params
        }});
    }
}

sub ws_send_roster {
    my ($self) = @_;

    my @raw_contacts = map {
        {
            jid => $_->jid,
            name => $_->name,
            groups => [$_->groups],
            presence => pretty_presence($_->get_priority_presence),
            subscription => $_->subscription,
        }
    } $self->xmpp->get_roster->get_contacts;
    $self->ws_send('roster', contacts => \@raw_contacts);
}

sub guard_connected {
    my ($self) = @_;
    if (!$self->xmpp || !$self->xmpp->is_connected) {
        $self->ws_send('error', message => 'not connected');
        return;
    }
    return 1;
}

1;