You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vmnotify/vmnotify

326 lines
8.7 KiB

#!/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 Data::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
<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 dbaliases where alias_username=?"
) 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 Data::UUID->create_str();
}
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: context=%s new=%d old=%d urgent=%d
EOF
@data{qw(mailbox context new old urgent)};
chomp $log_str;
$macros->{bodylen} = 2+length($macros->{body_mw})+2+length($macros->{body_vm})+2;
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: context=%s new=%d old=%d urgent=%d
EOF
@data{qw(mailbox context new old urgent)};
chomp $log_str;
$macros->{bodylen} = 2+length($macros->{body_mw})+2+length($macros->{body_vm})+2;
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 = new LWP::UserAgent;
$ua->agent('NGCP vmnotify 1.0');
$ua->ssl_opts(
verify_hostname => 0,
SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
);
my $r = new HTTP::Request("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 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 new old urgent)) {
$data{$arg} = $ARGV[$idx] // 0;
$idx++;
}
my $extended = 1;
foreach my $arg (qw(msgnum from date duration)) {
$data{$arg} = $ARGV[$idx] or $extended = 0;
$idx++;
}
$data{callid} = gen_callid().'@voip.sipwise.local';
($data{user}, $data{domain}) = get_user_domain($data{mailbox});
my $etag = get_etag($data{user}, $data{domain});
my $sipifmatch = defined $etag ? "SIP-If-Match: $etag" : "STRIP";
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,
);
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 ($EVAL_ERROR) {
$log->error($EVAL_ERROR);
exit 1;
}
return;
}
main();
exit 0;