Initial commit
commit
44caaf09df
|
@ -0,0 +1,4 @@
|
|||
config.env
|
||||
conf/orig
|
||||
rec
|
||||
*.swp
|
|
@ -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
|
|
@ -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*";
|
||||
}
|
||||
}
|
|
@ -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')
|
|
@ -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)
|
|
@ -0,0 +1,5 @@
|
|||
[general]
|
||||
|
||||
[logfiles]
|
||||
console => notice,warning,error
|
||||
messages => notice,warning,error
|
|
@ -0,0 +1,2 @@
|
|||
[modules]
|
||||
autoload=yes
|
|
@ -0,0 +1,6 @@
|
|||
[general]
|
||||
|
||||
[default]
|
||||
mode=files
|
||||
directory=moh
|
||||
sort=random
|
|
@ -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"}}
|
|
@ -0,0 +1,6 @@
|
|||
[template]
|
||||
src = "sip.conf.tmpl"
|
||||
dest = "/etc/asterisk/sip.conf"
|
||||
keys = [
|
||||
"/asterisk",
|
||||
]
|
|
@ -0,0 +1,2 @@
|
|||
ASTERISK_TRUNK_USERNAME=4821372137
|
||||
ASTERISK_TRUNK_SECRET=dupadupadupa
|
|
@ -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"
|
|
@ -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
|
Loading…
Reference in New Issue