Initial commit

master
informatic 2018-03-24 13:18:37 +01:00
commit 44caaf09df
13 changed files with 533 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
config.env
conf/orig
rec
*.swp

12
Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM dougbtv/asterisk14
RUN yum install -y perl-Digest-MD5 perl-libwww-perl perl-LWP-Protocol-https sox mpg123 which python-pip
RUN pip install pyst2==0.5.0
RUN curl -L https://github.com/kelseyhightower/confd/releases/download/v0.15.0/confd-0.15.0-linux-amd64 > /usr/local/bin/confd && chmod +x /usr/local/bin/confd
COPY conf/*.conf /etc/asterisk/
COPY conf/*.conf.tmpl /etc/confd/templates/
COPY conf/*.conf.toml /etc/confd/conf.d/
COPY agi/* /var/lib/asterisk/agi-bin/
CMD confd -onetime -backend env && asterisk -fvv

372
agi/googletts.agi Executable file
View File

@ -0,0 +1,372 @@
#!/usr/bin/env perl
#
# AGI script that uses Google's translate text to speech engine.
#
# Copyright (C) 2011 - 2015, Lefteris Zafiris <zaf@fastmail.com>
#
# This program is free software, distributed under the terms of
# the GNU General Public License Version 2. See the COPYING file
# at the top of the source tree.
#
# -----
# Usage
# -----
# agi(googletts.agi,"text",[language],[intkey],[speed]): This will invoke the Google TTS
# engine, render the text string to speech and play it back to the user.
# If 'intkey' is set the script will wait for user input. Any given interrupt keys will
# cause the playback to immediately terminate and the dialplan to proceed to the
# matching extension (this is mainly for use in IVR, see README for examples).
# If 'speed' is set the speech rate is altered by that factor (defaults to 1.2).
#
# The script contacts google's TTS service in order to get the voice data
# which then stores in a local cache (by default /tmp/) for future use.
#
# Parameters like default language, sample rate, caching and cache dir
# can be set up by altering the following variables:
# Default langeuage: $lang
# Sample rate: $samplerate
# Speed factor: $speed
# Chace: $usecache
# Chache directory: $cachedir
# SoX Version: $sox_ver
#
use warnings;
use strict;
use utf8;
use Encode qw(decode encode);
use File::Temp qw(tempfile);
use File::Copy qw(move);
use File::Path qw(mkpath);
use Digest::MD5 qw(md5_hex);
use URI::Escape;
use LWP::UserAgent;
use LWP::ConnCache;
$| = 1;
# ----------------------------- #
# User defined parameters: #
# ----------------------------- #
# Default language #
my $lang = "en";
# Output speed factor #
my $speed = 1;
# Use of cache mechanism #
my $usecache = 1;
# Cache directory path #
my $cachedir = "/tmp";
# Output audio sample rate #
# Leave blank to auto-detect #
my $samplerate = "";
# SoX Version #
# Leave blank to auto-detect #
my $sox_ver = "";
# Verbose debugging messages #
my $debug = 0;
# ----------------------------- #
my %AGI;
my @text;
my $fh;
my $tmpname;
my $fexten;
my $intkey = "";
my $tmpdir = "/tmp";
my $maxlen = 4096;
my $timeout = 10;
my $url = "https://translate.google.com";
my $sox = `/usr/bin/which sox`;
my $mpg123 = `/usr/bin/which mpg123`;
# Store AGI input #
($AGI{arg_1}, $AGI{arg_2}, $AGI{arg_3}, $AGI{arg_4}) = @ARGV;
while (<STDIN>) {
chomp;
last if (!length);
$AGI{$1} = $2 if (/^agi_(\w+)\:\s+(.*)$/);
}
my $name = " -- $AGI{request}:";
# Abort if required programs not found. #
fatal_log("sox or mpg123 is missing. Aborting.") if (!$sox || !$mpg123);
chomp($sox, $mpg123);
# Sanitising input #
$AGI{arg_1} = decode('utf8', $AGI{arg_1});
for ($AGI{arg_1}) {
s/[\\|*~<>^\(\)\[\]\{\}[:cntrl:]]/ /g;
s/\s+/ /g;
s/^\s|\s$//g;
fatal_log("No text passed for synthesis.") if (!length);
# Split input to comply with google tts requirements #
@text = /.{1,150}$|.{1,150}[.,?!:;]|.{1,150}\s/g;
}
my $lines = @text;
# Setting language, interrupt keys and speed rate #
if (length($AGI{arg_2})) {
if ($AGI{arg_2} =~ /^[a-zA-Z]{2}(-[a-zA-Z]{2,6})?$/) {
$lang = $AGI{arg_2};
} else {
console_log("Invalid language setting. Using default.");
}
}
if (length($AGI{arg_3})) {
$intkey = "0123456789#*" if ($AGI{arg_3} eq "any");
$intkey = $AGI{arg_3} if ($AGI{arg_3} =~ /^[0-9*#]+$/);
}
if (length($AGI{arg_4})) {
$speed = $AGI{arg_4} if ($AGI{arg_4} =~ /^\d+(\.\d+)?$/);
}
# Check cache path size: dir length + md5 + file extension #
if ($usecache) {
if ((length($cachedir) + 32 + 6) < $maxlen) {
mkpath("$cachedir") unless (-d "$cachedir");
} else {
console_log("Cache path size exceeds limit. Disabling cache.");
$usecache = 0;
}
}
# Answer channel if not already answered #
print "CHANNEL STATUS\n";
my @result = checkresponse();
if ($result[0] == 4) {
print "ANSWER\n";
@result = checkresponse();
if ($result[0] != 0) {
fatal_log("Failed to answer channel.");
}
}
# Setting filename extension according to sample rate. #
if (!$samplerate) { ($fexten, $samplerate) = detect_format(); }
elsif ($samplerate == 12000) { $fexten = "sln12"; }
elsif ($samplerate == 16000) { $fexten = "sln16"; }
elsif ($samplerate == 32000) { $fexten = "sln32"; }
elsif ($samplerate == 44100) { $fexten = "sln44"; }
elsif ($samplerate == 48000) { $fexten = "sln48"; }
else { ($fexten, $samplerate) = ("sln", 8000); }
# Initialise User angent #
my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 1 });
$ua->agent("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:52.0) Gecko/20100101 Firefox/52.0");
$ua->env_proxy;
$ua->conn_cache(LWP::ConnCache->new());
$ua->timeout($timeout);
for (my $i = 0; $i < $lines; $i++) {
my $filename;
my $res;
my $len = length($text[$i]);
my $line = encode('utf8', $text[$i]);
$line =~ s/^\s+|\s+$//g;
next if (length($line) == 0);
if ($debug) {
console_log("Text passed for synthesis: $line",
"Language: $lang, Interrupt keys: $intkey, Sample rate: $samplerate",
"Speed: $speed, Caching: $usecache, Cache dir: $cachedir"
);
}
if ($usecache) {
$filename = md5_hex("$line.$lang.$speed");
# Stream file from cache if it exists #
if (-r "$cachedir/$filename.$fexten") {
console_log("File already in cache.") if ($debug);
$res = playback("$cachedir/$filename", $intkey);
die if ($res < 0);
last if ($res > 0);
next;
}
}
# Hnadle interrupts #
$SIG{'INT'} = \&int_handler;
$SIG{'HUP'} = \&int_handler;
($fh, $tmpname) = tempfile("ggl_XXXXXXXX", DIR => $tmpdir, UNLINK => 1);
my $token = make_token($line);
$line = uri_escape($line);
my $req = "$url/translate_tts?ie=UTF-8&q=$line&tl=$lang&total=$lines&idx=$i&textlen=$len&client=tw-ob&tk=$token&prev=input";
console_log("URL passed: $req") if ($debug);
my $ua_request = HTTP::Request->new('GET' => $req);
$ua_request->header(
'Accept' => '*/*',
'Accept-Encoding' => 'identity;q=1, *;q=0',
'Accept-Language' => 'en-US,en;q=0.5',
'DNT' => '1',
'Range' => 'bytes=0-',
'Referer' => 'https://translate.google.com/',
);
my $ua_response = $ua->request($ua_request, $tmpname);
fatal_log("Failed to fetch file: ", $ua_response->code, $ua_response->message) unless ($ua_response->is_success);
# Convert mp3 file to 16bit 8Khz or 16kHz mono raw #
system($mpg123, "-q", "-w", "$tmpname.wav", $tmpname) == 0
or fatal_log("$mpg123 failed: $?");
# Detect sox version #
if (!$sox_ver) {
$sox_ver = (system("$sox --version > /dev/null 2>&1") == 0) ? 14 : 12;
console_log("Found sox version $sox_ver in: $sox, mpg123 in: $mpg123") if ($debug);
}
my @soxargs = get_sox_args("$tmpname.wav", "$tmpname.$fexten");
system(@soxargs) == 0 or fatal_log("$sox failed: $?");
unlink "$tmpname.wav";
# Playback and save file in cache #
$res = playback($tmpname, $intkey);
die if ($res < 0);
if ($usecache) {
console_log("Saving file $filename to cache") if ($debug);
move("$tmpname.$fexten", "$cachedir/$filename.$fexten");
} else {
unlink "$tmpname.$fexten";
}
last if ($res > 0);
}
exit;
sub checkresponse {
my $input = <STDIN>;
my @values;
chomp $input;
if ($input =~ /^200 result=(-?\d+)\s?(.*)$/) {
@values = ("$1", "$2");
} else {
$input .= <STDIN> if ($input =~ /^520-Invalid/);
warn "$name Unexpected result: $input\n";
@values = (-1, -1);
}
return @values;
}
sub playback {
my ($file, $keys) = @_;
my @response;
print "STREAM FILE $file \"$keys\"\n";
@response = checkresponse();
if ($response[0] >= 32 && chr($response[0]) =~ /[\w*#]/) {
console_log("Got digit chr($response[0])") if ($debug);
print "SET EXTENSION ", chr($response[0]), "\n";
checkresponse();
print "SET PRIORITY 1\n";
checkresponse();
} elsif ($response[0] == -1) {
console_log("Failed to play $file.");
}
return $response[0];
}
sub detect_format {
# Detect the sound format used #
my @format;
print "GET FULL VARIABLE \${CHANNEL(audionativeformat)}\n";
my @reply = checkresponse();
for ($reply[1]) {
if (/(silk|sln)12/) { @format = ("sln12", 12000); }
elsif (/(speex|slin|silk)16|g722|siren7/) { @format = ("sln16", 16000); }
elsif (/(speex|slin|celt)32|siren14/) { @format = ("sln32", 32000); }
elsif (/(celt|slin)44/) { @format = ("sln44", 44100); }
elsif (/(celt|slin)48/) { @format = ("sln48", 48000); }
else { @format = ("sln", 8000); }
}
return @format;
}
sub get_sox_args {
# Set the appropiate sox cli arguments #
my ($source_file, $dest_file) = @_;
my @soxargs = ($sox, $source_file, "-q", "-r", $samplerate, "-t", "raw", $dest_file);
if ($speed != 1) {
if ($sox_ver >= 14) {
push(@soxargs, ("tempo", "-s", $speed));
} else {
push(@soxargs, ("stretch", 1 / $speed, "80"));
}
}
return @soxargs;
}
# Obfuscated crap straight from Google:
# https://translate.google.com/translate/releases/twsfe_w_20151214_RC03/r/js/desktop_module_main.js
sub make_token {
my $text = shift;
my $time = int(time() / 3600);
my @chars = unpack('U*', $text);
my $stamp = $time;
foreach (@chars) {
$stamp = make_rl($stamp + $_, '+-a^+6');
}
$stamp = make_rl($stamp, '+-3^+b+-f');
if ($stamp < 0) {
$stamp = ($stamp & 2147483647) + 2147483648;
}
$stamp %= 10**6;
return ($stamp . '.' . ($stamp ^ $time));
}
sub make_rl {
my ($num, $str) = @_;
for (my $i = 0; $i < length($str) - 2 ; $i += 3) {
my $d = substr($str, $i+2, 1);
if (ord($d) >= ord('a')) {
$d = ord($d) - 87;
} else {
$d = int($d);
}
if (substr($str, $i+1, 1) eq '+') {
$d = $num >> $d;
} else {
$d = $num << $d;
}
if (substr($str, $i, 1) eq '+') {
$num = $num + $d & 4294967295;
} else {
$num = $num ^ $d;
}
}
return $num;
}
sub console_log {
foreach my $message (@_) {
warn "$name $message\n";
print "NOOP \"$name $message\"\n";
checkresponse();
}
}
sub fatal_log {
console_log(@_);
die;
}
sub int_handler {
die "$name Interrupt signal received, terminating...\n";
}
END {
if ($tmpname) {
warn "$name Cleaning temp files.\n" if ($debug);
unlink glob "$tmpname*";
}
}

75
agi/phonetab.agi Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python
import datetime
import sys
from asterisk.agi import AGI
# Use next matched number after TIMEOUT seconds
TIMEOUT = 30
def part_match(value, e):
"""Matches single part of cron-like range expression"""
if e == '*':
return True
elif '/' in e:
everything, diff = e.split('/')
return part_match(value, everything) and value % int(diff) == 0
elif '-' in e:
start, end = map(int, e.split('-', 1))
return value >= start and value <= end
else:
return int(e) == value
def cron_match(value, expr):
"""Matches cron-like range expression"""
return any(part_match(value, e) for e in expr.split(','))
def cronline_match(dt, expr):
"""Matches cron-like expression to provided datetime"""
return all([
cron_match(t, e)
for t, e in
zip([dt.minute, dt.hour, dt.day, dt.month, dt.weekday()], expr.split())
])
def phonetab_list(fname, dt):
"""Returns matches for provided datetime from phonetab file"""
with open(fname) as fd:
return [
line.rsplit(None, 1)[1]
for line in fd
if line.strip()
and not line.startswith('#')
and cronline_match(dt, line.rsplit(None, 1)[0])
]
if __name__ == "__main__":
agi = AGI()
agi.verbose('Testing shite')
numbers = phonetab_list(sys.argv[1], datetime.datetime.now())
agi.verbose(str(numbers))
for num in numbers:
# Default to SIP/...@trunk-out if no type specified, multiple numbers
# can be comma-separated
# Example:
# 123456789,SIP/2137@local -> SIP/123456789@trunk-out&SIP/2137@local
numlist = [(n if '/' in n else 'SIP/%s@trunk-out' % n)for n in num.split(',')]
dialpath = '&'.join(numlist)
agi.verbose('Dialing %s' % (dialpath,))
res = agi.appexec('Dial', '%s,%d,m' % (dialpath, TIMEOUT))
agi.verbose('Response: %r' % res)
dialstatus = agi.get_variable('DIALSTATUS')
agi.verbose('Dialstatus: %r' % dialstatus)
if dialstatus == 'ANSWER':
break
agi.appexec("Agi", 'googletts.agi,\\"All lines are currently busy. Please try again later.\\",en')
agi.verbose('Number timed out, next...')
else:
agi.verbose('No more numbers')

6
conf/extensions.conf Normal file
View File

@ -0,0 +1,6 @@
[trunk-in]
exten => s,1,Answer()
;same => n,SetMusicOnHold(default)
same => n,Monitor(wav,rec-${STRFTIME(${EPOCH},,%d%m%Y-%H:%M:%S)}-${CALLERID(num)})
same => n,agi(googletts.agi,"This is BGP.WTF abuse phone number. This call will be recorded.",en)
same => n,agi(phonetab.agi,/etc/asterisk/phonetab)

5
conf/logger.conf Normal file
View File

@ -0,0 +1,5 @@
[general]
[logfiles]
console => notice,warning,error
messages => notice,warning,error

2
conf/modules.conf Normal file
View File

@ -0,0 +1,2 @@
[modules]
autoload=yes

6
conf/musiconhold.conf Normal file
View File

@ -0,0 +1,6 @@
[general]
[default]
mode=files
directory=moh
sort=random

25
conf/sip.conf.tmpl Normal file
View File

@ -0,0 +1,25 @@
[general]
context=sip
nat=force_rport,comedia
register => {{getv "/asterisk/trunk/username"}}:{{getv "/asterisk/trunk/secret"}}@sip.2call.pl
[trunk-out]
type=friend
disallow=all
allow=alaw
allow=ulaw
insecure=invite
canreinvite=no
context=trunk-in
qualify=no
fromdomain=sip.2call.pl
host=sip.2call.pl
defaultuser={{getv "/asterisk/trunk/username"}}
fromuser={{getv "/asterisk/trunk/username"}}
username={{getv "/asterisk/trunk/username"}}
user={{getv "/asterisk/trunk/username"}}
secret={{getv "/asterisk/trunk/secret"}}

6
conf/sip.conf.toml Normal file
View File

@ -0,0 +1,6 @@
[template]
src = "sip.conf.tmpl"
dest = "/etc/asterisk/sip.conf"
keys = [
"/asterisk",
]

2
config.env.dist Normal file
View File

@ -0,0 +1,2 @@
ASTERISK_TRUNK_USERNAME=4821372137
ASTERISK_TRUNK_SECRET=dupadupadupa

13
docker-compose.yml Normal file
View File

@ -0,0 +1,13 @@
version: '2'
services:
asterisk:
build: .
network_mode: "host"
env_file:
- config.env
# Uncomment this to store recordings in host directory
# volumes:
# - "./rec:/var/spool/asterisk/monitor"
volumes:
- "./phonetab:/etc/asterisk/phonetab"
- "./moh:/var/lib/asterisk/moh"

5
phonetab.dist Normal file
View File

@ -0,0 +1,5 @@
# <Minute> <Hour> <Day_of_the_Month> <Month_of_the_Year> <Day_of_the_Week>
* 8-20 * * 0-4 0048213721372
* 0-7,21-24 * * * 0048424242424
* * * * * 0048213721372