#!/usr/bin/perl -w
########################################################################
# vmnotify - An Asterisk-VoiceMail compatible MWI notification script
# (c) 2017 Sipwise GmbH. All rights reserved.
#
# Author: Kirill Solomko <ksolomko@sipwise.com>
#
########################################################################
use strict;
use warnings;
use English;
use Readonly;
use Log::Log4perl qw(:easy);
use POSIX qw(strftime);
use Readonly;
use Config::Any;
use Sys::Syslog qw(:DEFAULT :macros setlogsock);
use IO::Socket;
use UUID;
use Data::Dumper;
use HTTP::Request;
use LWP::UserAgent;
use JSON;
use IO::Socket::SSL;
use DBI;

unless (scalar @ARGV) {
    print <<USAGE;
==
    vmnotify - An Asterisk-VoiceMail compatible MWI notification script
==
$PROGRAM_NAME [options]
Options:
    [basic]
    <context>         - voicemail context (default: "default")
    <mailbox>         - mailbox number
    <uuid>            - subscriber uuid
    <new messages>    - amount of new messages
    [old messages]    - amount of old messages
    [urgent messages] - amount of urgent messages

    [extended]
    <msgnum>          - message number
    <from>            - from user
    <date>            - datetime string
    <duration>        - mesasge duration
USAGE
    exit 0;
}

Readonly my $CONF_FILE => '/etc/ngcp-vmnotify/vmnotify.conf';

my %CONFIG = %{
     Config::Any->load_files({
        files   => [ $CONF_FILE ],
        use_ext => 1
     })->[0]->{$CONF_FILE}
        or do {
            log_syslog(
                "$PROGRAM_NAME error: Cannot load config $CONF_FILE: $ERRNO");
                exit 1;
     };
};

my $debug = $CONFIG{DEBUG} ? "DEBUG" : "INFO";

Log::Log4perl->init(\<<EOF);
log4perl.category.vmnotify=$debug, SYSLOG, SCREEN

log4perl.appender.SYSLOG=Log::Dispatch::Syslog
log4perl.appender.SYSLOG.facility=local0
log4perl.appender.SYSLOG.ident=vmnotify
log4perl.appender.SYSLOG.layout=PatternLayout
log4perl.appender.SYSLOG.layout.ConversionPattern=%-5p %m%n

log4perl.appender.SCREEN=Log::Log4perl::Appender::Screen
log4perl.appender.SCREEN.mode=append
log4perl.appender.SCREEN.layout=PatternLayout
log4perl.appender.SCREEN.layout.ConversionPattern=%-5p %m%n
EOF

my $log = Log::Log4perl->get_logger("vmnotify");
my %data = ();

my $dsn = "DBI:mysql:database=kamailio;host=$CONFIG{DB_HOST};port=$CONFIG{DB_PORT}";
my $dbh = DBI->connect($dsn, $CONFIG{DB_USER}, $CONFIG{DB_PASS}) or die
    "Failed to connect to central db: $DBI::errstr";
my $sth_etag = $dbh->prepare(
    "select etag from presentity where username=? and domain=? ".
    "and event='message-summary' order by received_time desc limit 1"
) or die "Failed to prepare etag query: $DBI::errstr";
my $sth_userdom = $dbh->prepare(
    "select username, domain from subscriber where uuid=?"
) or die "Failed to prepare userdom query: $DBI::errstr";

#----------------------------------------------------------------------
sub log_syslog {
    my $str = shift;

    setlogsock "native", "unix", "udp";
    openlog $PROGRAM_NAME, "pid", LOG_DEBUG;
    syslog LOG_ERR, '%s', $str ;

    return;
}

sub gen_callid {
    my $self = shift;

    return UUID::uuid();
}

sub gen_branchid {
    my @list = ("a".."z",0..9,"A".."Z");
    return "z9hG4bK" . join "",
        map { $list[int(rand($#list))] } (1..8);
}

sub load_mwi_file {
    my $path = shift;
    open(my $fh, "<", $path)
        or die "Cannot open file '$path': $!";
    binmode $fh;
    my $mwi;
    while (<$fh>) {
        $_ =~ s/([^\r])\n/$1\r\n/;
        $mwi .= $_;
    }
    close $fh;
    return $mwi; 
}

sub send_mwi {
    my $log_str = shift;
    my $mwi = shift;

    my $sock = IO::Socket::INET->new(PeerAddr => $CONFIG{SERVER},
                                     LocalAddr => $CONFIG{LOCAL_IP},
                                     Proto    => 'udp',
                                     TimeOut  => 3,
                                     Blocking => 0)
        or die sprintf "Cannot send %s server=%s error=%s\n",
            $log_str, $CONFIG{SERVER}, $ERRNO;

    $sock->send($mwi)
        or die sprintf "Cannot send %s error=%s\n", $log_str, $ERRNO;

    $sock->close();
}

sub send_mwi_notify {
    my $macros = shift;
    my $mwi = load_mwi_file($CONFIG{SIPFILE});
    die "Empty MWI. Cannot send SIP MWI notification\n" unless $mwi;

    my $log_str = sprintf <<EOF,
vmnotify to=%s uuid=%s: context=%s new=%d old=%d urgent=%d
EOF
        @data{qw(mailbox uuid context new old urgent)};
    chomp $log_str;

    $macros->{bodylen} = length($macros->{body_mw})+2+length($macros->{body_vm})+4;
    map { $mwi =~ s/\$$_\$/$macros->{$_}/gi; } keys %{ $macros };
    send_mwi($log_str, $mwi);

    $log->debug($log_str);

    return;
}

sub send_mwi_publish {
    my $macros = shift;
    my $mwi = load_mwi_file($CONFIG{SIPPUBLISHFILE});
    die "Empty PUBLISH MWI. Cannot publish SIP MWI notification\n" unless $mwi;

    my $log_str = sprintf <<EOF,
vmnotify to=%s uuid=%s: context=%s new=%d old=%d urgent=%d
EOF
        @data{qw(mailbox uuid context new old urgent)};
    chomp $log_str;

    $macros->{bodylen} = length($macros->{body_mw})+2+length($macros->{body_vm})+4;
    map { $mwi =~ s/\$$_\$/$macros->{$_}/gi; } keys %{ $macros };
    $mwi =~ s/\r\nSTRIP\r\n/\r\n/g;
    $log->debug($mwi);
    send_mwi($log_str, $mwi);

    $log->debug($log_str);

    return;
}

sub send_ext_notify {

    my $url = $CONFIG{EXT_SERVER_URL} || return;

    my %url_ph = (
        prefix => 'voicemail',
        suffix => 'notify',
        caller => $data{from},
        callee => $data{mailbox},
        callid => $data{callid},
        token  => '',
    );

    my $mm = 0;
    foreach my $v (qw(prefix suffix caller callee callid token)) {
        my $t = $url_ph{$v} ? $url_ph{$v}."/" : "";
        if ($url =~ s/\$\{$v\}/$t/g) {
            $mm = 1;
        }
    }
    $url = substr($url, 0, -1) if $mm;

    my $ua = LWP::UserAgent->new;
    $ua->agent('NGCP vmnotify 1.0');

    $ua->ssl_opts(
        verify_hostname => 0,
        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
    );

    my $r = HTTP::Request->new("POST", $url);
    $r->header('Content-Type', 'application/json');

    my $json = {
        caller       => $data{from},
        callee       => $data{mailbox},
        recording_id => $data{msgnum},
        timestamp    => $data{date},
        duration     => $data{duration},
    };

    $r->content(encode_json $json);

    my $res = $ua->request($r);

    my $log_str = sprintf <<EOF,
ext vmnotify url=%s from=%s to=%s msgnum=%d date=%s duration=%d
EOF
        $url, @data{qw(from user msgnum date duration)};
    chomp $log_str;

    if ($res->is_success) {
        $log->debug($log_str);
    } else {
        die sprintf "Cannot send %s error=%s\n", $log_str, $res->status_line
    }

    return;
}

sub transcribe {
    my $msgnum = 0 + $data{msgnum};
    my $fh;
    open($fh, '-|', "ngcp-invoke-task --task transcribe_voicemail --dst localhost --data " .
    	"'{\"msgnum\":$msgnum,\"uuid\":\"$data{uuid}\"}' 2>&1 < /dev/null");
    my @out = <$fh>;
    close($fh);
    my $status = $?;
    if ($status) {
        $log->warn("Failed to invoke transcription, exit code $status, output: @out");
    }
}

sub get_etag {
        my $user = shift;
        my $domain = shift;
        $sth_etag->execute($user, $domain)
            or die "Failed to load ETag for $user\@$domain";
        my ($etag) = $sth_etag->fetchrow_array();
        $sth_etag->finish();
        return $etag;
}

sub get_user_domain {
        my $alias = shift;
        $sth_userdom->execute($alias)
            or die "Failed to load user and domain for $alias";
        my ($user, $domain) = $sth_userdom->fetchrow_array();
        $sth_userdom->finish();
        return ($user, $domain);
}

sub main {
    eval {
        die "Incorrect arguments list" if $#ARGV < 2;

        my $idx = 0;
        foreach my $arg (qw(context mailbox uuid new old urgent)) {
            $data{$arg} = $ARGV[$idx] // 0;
            $idx++;
        }

        my $extended = 1;
        foreach my $arg (qw(msgnum from date duration)) {
            defined($data{$arg} = $ARGV[$idx]) or $extended = 0;
            $idx++;
        }

        my $num_args = scalar @ARGV;
        for (my $i = 10; $i < $num_args; $i += 3) {
            my $arg1 = $ARGV[$i];   # action: (a)dd/(r)ead/(d)elete
            my $arg2 = $ARGV[$i+1]; # id
            my $arg3 = $ARGV[$i+2]; # action

            my %charToWord = (
                'a' => 'add',
                'r' => 'read',
                'd' => 'delete',
                'u' => 'undelete',
                'm' => 'moved',
                'x' => 'unread',
            );

            $arg1 = $charToWord{$arg1} // 0;

            if ($arg2 =~ /^[a-zA-Z0-9-]+$/) {
                if (exists $data{vm}{$arg2}) {
                    push @{$data{vm}{$arg2}}, {
                        action => $arg1,
                        id => $arg2,
                        callid => $arg3
                    };
                } else {
                    $data{vm}{$arg2} = [{
                        action => $arg1,
                        id => $arg2,
                        callid => $arg3
                    }];
                }
            }
        }

        $data{callid} = gen_callid().'@voip.sipwise.local';
        ($data{user}, $data{domain}) = get_user_domain($data{uuid});
        my $etag = get_etag($data{user}, $data{domain});
        my $sipifmatch = defined $etag ? "SIP-If-Match: $etag" : "STRIP";
        my $action;
        foreach my $key (sort keys %{$data{'vm'}}) {
            my @values = map { "P-NGCP-MWI-action: $_->{action} $_->{id} $_->{callid}" } @{$data{'vm'}{$key}};
            $action .= "\r\n".join("\r\n", @values);
        }

        my %macros = (
            body_mw => "Messages-Waiting: ". ($data{new} ? "yes" : "no"),
            body_vm => "Voice-Message: $data{new}/$data{old} ($data{urgent}/0)",
            call_id => $data{callid},
            branch  => gen_branchid(),
            user    => $data{user},
            domain  => $data{domain},
            sipifmatch => $sipifmatch,
            mwi_action => $action
        );

        send_mwi_notify(\%macros);

        # use a different call-id for publish vs the notify above to make traces more clear
        # that they actually don't belong together
        $macros{call_id} = gen_callid().'@voip.sipwise.local';
        send_mwi_publish(\%macros);

        if ($extended &&
            $CONFIG{EXT_NOTIFY} && $CONFIG{EXT_NOTIFY} eq "yes") {
            send_ext_notify();
        }

        if ($extended &&
            $CONFIG{TRANSCRIBE} && $CONFIG{TRANSCRIBE} eq "yes") {
            transcribe();
        }
    };
    if ($EVAL_ERROR) {
        $log->error($EVAL_ERROR);
        exit 1;
    }

    return;
}

main();

exit 0;

