Реализация аудиоконференций в Telegram + Asterisk



    В предыдущей статье я описывал реализацию выбора пользователем места жительства при регистрации в моем telegram боте, который я создавал вдохновившись идеей «Телефонного эфира». В этой же статье я опишу интеграцию бота с Asterisk .

    Зачем?


    Многим не нравится что в Telegram нельзя осуществлять групповые звонки.

    Ну не использовать же Viber?

    Также есть ряд кейсов именно для такой реализации, например:

    • Для проведения анонимных аудиоконференций, когда не хочется «засветить» свой номер либо id среди участников конференции (сразу на ум приходит шабаш хакеров либо клуба анонимных алкоголиков). Не нужно находиться в какой либо группе, сообществе, канале
    • Когда не известно кто подключиться к конференции вообще, но нужно ограничить доступ паролем
    • Все прелести Asterisk: управление конференцией (mute/umute, kick), организация гибридных аудиоконференций с участием клиентов, зарегистрированных на asterisk, telegram и PSTN. Неплохо можно сэкономить на международных звонках
    • Организация корпоративного callback via telegram и т.п.

    На ум приходит куча вариантов, их много, ограничено лишь фантазией. После многих лет работы с Asterisk я считаю, что главное завести на него звонок, а дальше с ним можно сделать все что годно, хоть в космос отправить.

    Связка Asterisk VoIP- Telegram VoIP


    Сама связка VoIP реализована благодаря библиотеки tg2sip. Использование ее описано в самом репозитории в разделе Usage. Есть еще несколько статей по настройке. Даже есть Docker образ.
    Описание этой связки выходит за рамки данной статьи.

    Единственный нюанс который я хотел бы озвучить — это то, что нельзя позвонить на telegram_id, номера которого нет в Вашей книге контактов. Поэтому звонить нужно на номер телефона, на который зарегистрирован telegram.

    В моем боте реализованы как публичные аудиоконференции (Эфиры), к которым может подключиться любой желающий, так и приватные аудиоконференции с доступом по паролю. Приватные комнаты/пароли создают сами пользователи и могут использовать бот в качестве площадки для проведения аудиоконференций, совещаний и т.п.

    Взаимодействие telegram bot — Asterisk


    Схема взаимодействия в моем боте выглядит следующим образом.

    Пользователь выбирает желаемую комнату в меню бота, бот вызывает функцию взаимодействия с Asterisk по API передав в POST запросе параметры для подключения:

    • номер телефона абонента
    • идентификатор конференц комнаты
    • callerid для презентации в конференц комнате
    • язык для озвучивания пользователю уведомлений в системе Asterisk на родном языке

    Далее, Asterisk осуществляет исходящий звонок через каналы telegram на номер, указанный в параметрах запроса. После ответа пользоветелем на звонок, Asterisk подключает его в соответствующую комнату.

    Можно было бы использовать прямое подключение с бота на Astersik AMI, но я предпочитаю работать через API, чем проще — тем лучше.

    API на стороне Asterisk сервера


    Код простого API на python. Для инициализации звонка используются .call файлы

    #!/usr/bin/python3
    from flask import Flask, request, jsonify
    import codecs
    import json
    import glob
    import shutil
    
    api_key = "s0m3_v3ry_str0ng_k3y"
    app = Flask(__name__)
    
    @app.route('/api/conf', methods= ['POST'])
    def go_conf():
        content = request.get_json()
        ## Блок авторизации
        if not "api_key" in content:
            return jsonify({'error': 'Authentication required', 'message': 'Please specify api key'}), 401
        if not content["api_key"] == api_key:
            return jsonify({'error': 'Authentication failure', 'message': 'Wrong api key'}), 401
        ## Проверка наличия нужных параметров в запросе
        if not "phone_number" in content or not "room_name" in content or not "caller_id" in content:
            return jsonify({'error': 'not all parameters are specified'}), 400
    
        if not "lang" in content:
            lang = "ru"
        else:
            lang = content["lang"]
    
        phone_number = content["phone_number"]
        room_name = content["room_name"]
        caller_id = content["caller_id"]
        calls = glob.glob(f"/var/spool/asterisk/outgoing/*-{phone_number}*")
        callfile = "cb_conf-" + phone_number + "-" + room_name + ".call"
        filename = "/var/spool/asterisk/" + callfile
        if calls:
            return jsonify({'message': 'error', "text": "call already in progress"})
        with codecs.open(filename, "w", encoding='utf8') as f:
            f.write("Channel: LOCAL/" + phone_number + "@telegram-out\n")
            f.write("CallerID: <" + caller_id + ">\n")
            f.write("MaxRetries: 0\nRetryTime: 5\nWaitTime: 30\n")
            f.write("Set: LANG=" + lang + "\nContext: conf-in\n")
            f.write("Extension: " + room_name + "\nArchive: Yes\n")
        shutil.chown(filename, user="asterisk", group="asterisk")
        shutil.move(filename, "/var/spool/asterisk/outgoing/" + callfile)
        return jsonify({'message': 'ok'})
    
    
    if __name__ == '__main__':
        app.run(debug=True,host='0.0.0.0', port=8080)
    

    При этом диалплан Asterisk в простом виде выглядит следующим образом:

    [telegram-out]
    exten => _+.!,1,NoOp()
    same => n,Dial(SIP/${EXTEN}@telegram)
    
    exten => _X!,1,NoOp()
    same => n,Dial(SIP/+${EXTEN}@telegram)
    
    [conf-in]
    exten => _.!,1,NoOp()
    same => n,Answer()
    same => n,Wait(3)
    same => n,Playback(beep)
    same => n,Set(CHANNEL(language)=${LANG})
    same => n,ConfBridge(${EXTEN})
    same => n,Hangup
    

    Данное API можно использовать и в других кейсах, например, для организации той же callback кнопки «Перезвонить мне» и т.п.

    Функция вызова API


    telephony_api.py

    import requests, json
    
    # Заменить example.com на Ваш url
    url = "http://example.com:8080/api/conf"
    api_key = "s0m3_v3ry_str0ng_k3y"
    
    def go_to_conf(phone_number, room_name, caller_id, lang="ru"):
        payload = {}
        payload["phone_number"] = phone_number
        payload["room_name"] = room_name
        payload["caller_id"] = caller_id
        payload["lang"] = lang
        payload["api_key"] = api_key
    
        headers = {
            'content-type': "application/json",
            'cache-control': "no-cache",
            }
        try:
            response = requests.request("POST", url, data=json.dumps(payload), headers=headers, timeout=2, verify=False)
            if "call already in progress" in response.text:
                return False, "Ошибка. Звонок еще не завершен."
            elif "error" in response.text:
                print(response.text)
                return False, "Ошибка. Произошел сбой. Попробуйте позже."
            else:
                return True, response.text
        except:
            return False, "Ошибка. Произошел сбой. Попробуйте позже."
    

    Этих двух инструментов уже хватит для интеграции в Ваш бот, заворачивайте его в свою логику и используйте.

    Пример бота для инициализации вызова в конференц комнату


    #!/usr/bin/python3.6
    import telebot
    from telephony_api import go_to_conf
    bot = telebot.TeleBot("ВашTOKEN")
    pnone_number = "799999999999"# Ваш номер телефона, на который зарегистрирован telegram аккаунт
    
    @bot.message_handler(content_types=['text'])
    def main_text_handler(message):
        if message.text == "Подключи меня в аудиоконференцию":
            bot.send_message(message.chat.id, "Ok. Сейчас на telegram Вам прийдет звонок, ответив на который Вы будете подключены в аудиоконференцию")
            func_result, func_message = go_to_conf(pnone_number, "ROOM1", "Bob", "ru")
            if not func_result:
                bot.send_message(chat_id=message.chat.id, text=func_message)
    
    if __name__ == "__main__":)
       print("bot started")
       bot.polling(none_stop=True)
    

    В данном примере номер телефона задан статически, в реальности же можно например, делать запросы в базу на соответствие message.chat.id — номер телефона.

    Надеюсь, моя статья поможет кому-то создать крутые проекты и в свою очередь поделиться ними с сообществом.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 3

      +1

      Здравствуйте!
      Никогда не слышал о таком, но идея классная!
      А почему так странно payload передаете?
      Можно же работать с обычными питоновскими словарями и data=json.dumps(payload_dict) или даже json = payload_dict передавать в запрос

        0
        Согласен, выглядит не очень, давно когда-то этот кусок кода копировал с постмана. Спасибо что указали на уродство). Можно было бы еще и **kwargs, но в коде бота хочется меньше кода)
        0
        А в целом — код для статей лучше публиковать в Gitlab/Github…
        not "phone_number" in content or not "room_name" in content or not "caller_id" in content

        import operator as op
        
        get_required_fields = op.getitem('phone_number', 'room_name', 'caller_id')
        
        if not any(get_required_fields(content)):
            ...

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое