Звонок через нужный транк

Материал из Mindsellers
Перейти к: навигация, поиск


Постановка задачи[править]

Возникла задача - распределять исходящие звонки с Asterisk по транкам в соответствии с тем, чьему пулу принадлежит номер. В примитивной реализации достаточно просто добавить шаблоны телефонов по префиксам в маршруты FreePBX, однако, например, префикс 999 делят несколько операторов, а вообще в официальной таблице распределения DEF-номеров более 52 тысяч строк!

Было принято решение все-таки работать с выгрузкой именно номеров, благо Россвязьнадзор предоставляет возможность, хоть и не без костылей, выкачать CSV-файл с распределением диапазонов номеров.


Структура решения[править]

Структура придумалась такая: раз в N времени скачиваем csv-файл, выдираем из него необходимые поля и загружаем в базу. Когда начинается вызов в Asterisk, обращаемся к базе, получаем название оператора, сопоставляем с неким правилом и как-то запихиваем в нужный транк. Вроде звучит просто, но есть пара подводных камней:

1. Хочется получить совместимость не только с FreePBX, но и с чистым Asterisk, и любыми другими системами телефонии

2. Не хочется "ломать" логику диалплана FreePBX

3. Нужна простота настройки, так как заказ коммерческий, и нужно написать howto

4. Возможно, в будущем потребуется доработка, например, распределение исходящих звонков на городские номера в соответствии с регионом

Из вышесказанного следует, что:

1. Никакой работы с базой напрямую из диалплана

2. Изменения в диалплане должны быть минимальны, никаких extensions_override_freepbx

3. Кажется, пора расчехлять Python

Итоговая схема получилась такая: скрипт get.py скачивает CSV, парсит его, приводит названия операторов в формат nazvanie_operatora_mobilnoy_svyazi и все запихивает в БД. Вызывается по cron. Все настройки системы сведены в файл def.conf в стандартном ini-формате. Конфиг содержит в себе путь для сохранения файла, настройки подключения к базе и соответствие названий операторов из базы и префиксов, которые будут добавляться к набранному номеру. При звонке вызывается скрипт num.py, который на вход принимает номер в 10-значном формате, а на выходе отдает префикс по определенному оператору(например, 1 - мегафон, 2 - билайн, 3 - МТС) или некий дефолтный префикс, если для определенного оператора префикс не задан. Это позволяет изначально настроить систему только под большую тройку, а при необходимости добавлять всяких ущербов вроде Теле2 не внося никаких изменений в скрипты.

Листинги[править]

Листинг get.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
import csv
import pymysql
from configparser import SafeConfigParser
import os
import sys
import subprocess
from transliterate import translit, get_available_language_codes

def getconfig(conf='def.conf'): #функция работы с конфигурационным файлом
    configfile=os.path.abspath(os.path.dirname(__file__))+'/'+conf
    parser = SafeConfigParser()
    try:
	parser.read(configfile)
	params=dict()
	params['path']=parser.get('csv','path')
	params['host']=parser.get('mysql','host')
	params['db']=parser.get('mysql','db')
	params['dbuser']=parser.get('mysql','dbuser')
	params['dbpass']=parser.get('mysql','dbpass')
	return params
    except:
	print 'Cannot read config file!'
	exit()
def download(path): #Скачиваем реестр в CSV
    try:
	subprocess.call(['wget','--keep-session-cookies','-O',path,'https://rossvyaz.gov.ru/docs/articles/DEF-9x.csv'])
    except Exception as e:
	print 'Cannot download csv'
	print e
	exit()

def csvtomysql(path,host,db,dbuser,dbpass): #Переносим данные в Mysql
    operators=set() #объявляем множество для учета операторов
    connect=pymysql.connect(host=host, user=dbuser, passwd=dbpass, db=db,charset='utf8')
    cursor=connect.cursor()
    print 'Reading CSV, please wait for few minutes...'
    with open(path, 'rb') as csvfile:
	data = csv.reader(csvfile, delimiter=';', quotechar='|')
	for row in data:
		operator=row[4].decode('cp1251')   #читаем в win-1251
		operator=operator.lower()          #все буквы в нижний регистр
		operator=operator.replace(' ','_') #меняем пробелы на подчеркивания
		operator=operator.replace('"','')  #убираем кавычки
		operator=operator.replace("'","")
                operator=translit(operator,'ru',reversed=True) # переводим в транслит
		if str(row[0]).isdigit(): #заносим в базу
			prefix=row[0]     #только если в первой колонке цифры
			start=row[1]
			end=row[2]
			sql="replace into def(def,start,end,operator) values (\'" +prefix+"\',\'"+start+"\',\'"+end+"\',\'"+operator+"\')"
			operators.add(operator)
			cursor.execute(sql)
    connect.commit()
    connect.close()
    return operators # возвращаем список операторов


params=getconfig()
download(params['path'])
operators=csvtomysql(params['path'],params['host'],params['db'],params['dbuser'],params['dbpass'])
print 'Valid operators is: \n'
for operator in operators:
    print operator
    

Листинг num.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import print_function
import pymysql
from configparser import SafeConfigParser
import sys
import os
def getconfig(conf='def.conf'):
    configfile=os.path.abspath(os.path.dirname(__file__))+'/'+conf
    parser = SafeConfigParser()
    try:
	parser.read(configfile)
	params=dict()
	params['path']=parser.get('csv','path')
	params['host']=parser.get('mysql','host')
	params['db']=parser.get('mysql','db')
	params['dbuser']=parser.get('mysql','dbuser')
	params['dbpass']=parser.get('mysql','dbpass')
	operators=dict(parser.items('action'))
	return params,operators
    except Exception as e:
	sys.exit(1)





def mysql(path,host,db,dbuser,dbpass,num):
    connect=pymysql.connect(host=host, user=dbuser, passwd=dbpass, db=db,charset='utf8')
    cursor=connect.cursor()
    sql="select operator from def.def where def=\'"+num[0:3]+"\' and \'"+num[3::]+"\' between start and end limit 1"
    
    cursor.execute(sql)
    try:
	operator=cursor.fetchall()[0][0]
    except:
	sys.exit(1)
    connect.close()
    return operator


params,operators=getconfig()

try:
    num=sys.argv[1]
except:
    sys.exit(1)
operator=mysql(params['path'],params['host'],params['db'],params['dbuser'],params['dbpass'],num)
try:
     prefix=operators[operator]
except:
    prefix=operators['default']

if prefix != '':
    print(prefix, end='')


Листинг файла настроек

[csv]
path=/tmp/def.csv

[mysql]
host=localhost
db=def
dbuser=defuser
dbpass=defpassword

[action]
default=0
pao_megafon=1
pao_vympel-kommunikatsii=2
pao_mobilnye_telesistemy=3


Инструкция по установке[править]

apt install python-pip
pip install pymysql configparser transliterate
#ставим pip и зависимости скриптов
cd /opt
mkdir def
cd ./def
wget mindsellers.ru/def1.0.tar.gz
tar xvfz def1.0.tar.gz

#качаем и распаковываем

mysql -uroot -p
create database def;
CREATE USER 'defuser'@'localhost' IDENTIFIED BY 'defpassword';
GRANT ALL PRIVILEGES ON  `def` . * TO  'defuser'@'localhost' WITH GRANT OPTION ;
\q
mysql -udefuser -p def < def.sql

#создаем базу, пользователя и разворачиваем дамп базы

nano /opt/def/def.conf

#Изменяем параметры доступа к БД

#Запускаем скрипт

/opt/def/get.py

#получаем список всех операторов транслитом, добавляем в конфигурационный файл префиксы по операторам
#default подставляется в случае, если оператор не описан в конфигурационном файле

echo '05 14 * * * root /opt/def/get.py >> /var/log/def.log' >> /etc/crontab 

#добавляем в cron

nano /etc/asterisk/extensions_custom.conf

#правим диалплан
...
[from-internal-custom]
exten => _89X.,1,Set(prefix=0)
exten => _89X.,n,Set(prefix=${SHELL(/opt/def/num.py ${EXTEN:1})})
;скрипту передаем номер в 10-значном формате
exten => _89X.,n,Noop(${prefix})
exten => _89X.,n,Goto(from-internal,${prefix}${EXTEN},1)
;дописываем префикс и возвращаем звонок на стандартную логику FreePBX

asterisk -rx 'dialplan reload'

# применяем диалплан


Проверить работоспособность скриптов можно так:

/opt/def/num.py 9063448810

На выходе мы должны получить префикс для билайна. Внимание: для корректной работы SHELL в Asterisk, результат выдается без символа перевода каретки! Остается только прописать маршруты во FreePBX в соответствии с назначенными префиксами