Гаджиев Тамерлан
14.07.2023
29794

Слушатель AMI с возможностью фильтрования ивентов и отправкой уведомлений о звонке в телеграм

Статья описывает разработку слушателя AMI (Asterisk Manager Interface), который позволяет фильтровать события и отправлять уведомления о звонках в Telegram.
В статье рассматривается процесс подключения слушателя AMI к Asterisk, настройка фильтров для отслеживания нужных событий и отправки уведомлений в Telegram.
Такой слушатель AMI может быть полезен, например, для мониторинга работы call-центра или других телефонных систем, где необходимо отслеживать определенные события и быстро реагировать на них. Отправка уведомлений в Telegram позволяет получать информацию о звонках и других событиях в режиме реального времени, даже если вы не находитесь за компьютером или не имеете доступа к почте.


В статье используется:

golang: go1.18.4
asterisk: 16

FreePBX 14.0.16.9
библиотека: github.com/Syfaro/telegram-bot-api

Подготовка

  • Переходим в файл /etc/asterisk/manager_custom.conf и создаем там нового пользователя для ami
Создание нового пользователя
Рисунок 1 — Создание нового пользователя
  • После чего переходим в asterisk -rvvv и выполняем команду manager reload
  • Далее идем в telegram @BotFather и создаем там бота
  • Пишем команду /newbot
 Создание бота
Рисунок 2 — Создание бота
  • Придумываем имя для нашего бота. Далее пишем имя бота, в котором должно быть обязательно слово bot
Создание бота
Рисунок 3 — Создание бота
  • После чего получаем наш токен, который понадобится для отправки уведомлений

На этом подготовка закончена и можем приступить к написанию самого слушателя для AMI.

Слушатель АМИ

Структура проекта будет следующей:

ami

├── decorator

│   └── decorator.go

├── go.mod

├── go.sum

├── main.go

└── tg

    └── tg.go

Далее мы более подробно разберем, для чего нужны эти файлы:

  • Создаем папку ami

mkdir ami

  • Переходим в созданную папку

cd ami

  • Создаем новый go модуль

go mod init

  • Создаем папки и файлы, как в структуре

mkdir decorator

mkdir tg

touch decorator/decorator.go

touch tg/tg.go

Приступим к написанию функции слушателя

  • Открываем файл decorator/decorator.go vim decorator/decorator.go


package decorator

import (
        "fmt"
        "net"
        "strings"
)


type Handler func(string)


func Watcher(handler Handler, events ...string){
        login := "Action: Login\r\nUsername: amigo\r\nSecret: admin\r\n\r\n"
        conn, err := net.Dial("tcp", "127.0.0.1:5038")
        checkErr(err)
        defer conn.Close()
        _, err = conn.Write([]byte(login))
        welcome := make([]byte, 1024)
        _, err = conn.Read(welcome)
        checkErr(err)
        fmt.Println(string(welcome))
        checkErr(err)
        for {
                data := make([]byte, 1024)
                _, err = conn.Read(data)
                checkErr(err)
                text := strings.Split(string(data), "\r\n")
                for _, value := range text{
                        event := strings.Split(value, ":")
                        if event[0] == "Event"{
                                eventName := strings.TrimSpace(event[1])
                                for _, e := range events{
                                        if eventName == e{
                                                handler(string(data))
                                        }
                                }
                        }
                }
        }

}



func checkErr(err error){
        if err != nil{
                panic(err)
        }
}
Функция слушателя
Рисунок 4 — Функция слушателя
  • Импортируем необходимые для работы модули
import (
        "fmt"
        "net"
        "strings"
)
  • Объявляем новую функцию слушателя, которая принимает в себя handler (функция для обработки ивентов, переданных ей) и массив с ивентами, которые нужно отслеживать
func Watcher(handler Handler, events ...string)
Название функции обязательно с большой буквы, иначе вы не сможете импортировать функцию
  • Функция для обработки ошибок
func checkErr(err error){
        if err != nil{
                panic(err)
        }
}

   login := «Action: Login\r\nUsername: amigo\r\nSecret: admin\r\n\r\n» — переменная, содержащая логин/пасс для AMI

        conn, err := net.Dial(«tcp», «127.0.0.1:5038») — подключаемся к сокету

        checkErr(err) — обработка ошибки

        defer conn.Close() — указываем, что после выполнения функции нужно закрыть соединение с сокетом

        _, err = conn.Write([]byte(login)) — отправляем в сокет строку с авторизацией

        welcome := make([]byte, 1024) — создаем новый срез байтов

        _, err = conn.Read(welcome) — читаем, что нам послал сокет и записываем в заранее созданный срез

        checkErr(err) — обработка ошибки

        fmt.Println(string(welcome)) — выводим в консоль полученные с сокета данные, предварительно переведя их в строку методом string()

        checkErr(err) — обработка ошибки

for { // бесконечный цикл
                data := make([]byte, 1024) // создаем срез байтов с названием data
                _, err = conn.Read(data) // читаем то, что нам было отправлено в сокет
                checkErr(err) // обрабатываем ошибки
                text := strings.Split(string(data), "\r\n") // разделим полученное по переводу строки
               
// далее создаем бесконечный цикл, внутри которого мы будем принимать все, что нам прилетает от AMI.

for _, value := range text{ // перебираем полученный массив и записываем значения в переменную value
                        event := strings.Split(value, ":") // делим переменную value по : и записываем это в переменную event
                        if event[0] == "Event"{ // проверяем, что первый элемент event равен слову Event
                                eventName := strings.TrimSpace(event[1]) // удаляем пробел у второго элемента event и записываем в переменную eventName
                                for _, e := range events{ // перебираем массив events, который мы передали в эту функцию и записываем значения в переменную e
                                        if eventName == e{ // проверяем, равно ли значение переменной eventName значению переменной e
                                                handler(string(data)) // передаем данные в функцию обработчик
                                        }
                                }
                        }
                }

Проходимся по каждой строке и проверяем, если строка равна Event и названия ивента есть в том массиве, что мы передали в функцию, то вызываем handler, в который передаем все полученное сообщение целиком.

  • Установим библиотеку для работы с telegram

go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5

  • Далее открываем tg.go

vim tg/tg.go

  • Пишем туда следующий код:
package tg

import (
    "github.com/Syfaro/telegram-bot-api" // импортируем библиотеку для работы с тг
)



func SendMessage(chat_id int64, text string){ // создаем функцию для отправки сообщений. Она принимает chatid, в который нужно отправлять, а так же текст, который нужно отправить
        token := "abc" // вставляем токен, полученный на этапе подготовки
        bot, err := tgbotapi.NewBotAPI(token) // логинемся за бота, ранее созданного
        checkErr(err) // проверяем на ошибки
        msg := tgbotapi.NewMessage(chat_id, text) // создаем объект сообщения
        bot.Send(msg) // отправляем сообщение
}


func checkErr(err error){
        if err != nil{
                panic(err)
        }
}
Название функции обязательно с большой буквы, иначе вы не сможете импортировать функцию
Функция для telegram
Рисунок 5 — Функция для telegram
  • Далее открываем файл main.go

vim main.go

package main


import (
        "ami/decorator" // импортируем ранее созданный нами модуль
        "ami/tg" // импортируем ранее созданный нами модуль
        "strings"
)

func main(){
        events := []string{"Newchannel", "PeerStatus"} // создаем массив с ивентами, которые нужно мониторить
        decorator.Watcher(eventHandler,  events...) // запускаем функцию слушателя и передаем туда функцию для обработки и массив с ивентами
}


func eventHandler(event string){ // сама функция обработчик, которая принимает аргументами строку
        var list_of_employees = map[string]int64{"100":333333333} // создаем словарь внутренний номер - tgid для того, чтобы писать пользователю, когда на него придет звонок
        var cid string // создаем переменную для хранения номера звонящего
        var exten string // создаем переменную для хранения номера, на который позвонили
        e := strings.Split(event, "\r\n") // разделим полученное по переводу строки
        for _, value := range e{ // перебираем получившийся массив
                line := strings.Split(value, ":") // делим полученное значение по : 
                action := line[0] // записываем первый элемент массива в переменную action
                if action == "CallerIDNum"{ // проверяем равно ли значение переменной action тексту CallerIDNum
                        cid = strings.TrimSpace(line[1]) // удаляем пробелы у 2 элемента массива и записываем в переменную cid
                }
                if action == "Exten"{ // проверяем равно ли значение переменной action тексту Exten
                        exten = strings.TrimSpace(line[1]) // удаляем пробелы у 2 элемента массива и записываем в переменную exten 

                }
        }
        notify := "Звонок от: "+cid // создаем строку, которую отправим пользователю в тг
        tg.SendMessage(list_of_employees[exten], notify) // вызываем функцию для отправки сообщения в телеграм, передавая туда тгид из словаря list_of_employees берем значения по внутреннему номеру, на который позвонили и текст
}
Функция для сообщений
Рисунок 6 — Функция для сообщений
  • Сохраняем и запускаем файл main.go
go run main.go
  • Видим следующее сообщение
Запуск
Рисунок 7 — Запуск
  • Далее делаем вызов, я делаю со 101 на 100
Вызов
Рисунок 8 — Вызов

Вывод: Данный скрипт можно использовать для интеграций. Вместо отправки уведомлений в тг можно поднимать карточку в какой-либо срм или уведомлять на почту. Описан был простой способ без каких-либо дополнительных проверок. Для более лучшей работы хорошо было бы создать service файл для скрипта и запускать его, как службу через systemctl. Более подробно о ami events можно почитать тут https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+AMI+Events

Подписаться
Уведомить о
guest
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Остались вопросы?

Я - Виталий Шелест, менеджер компании Voxlink. Хотите уточнить детали или готовы оставить заявку? Укажите номер телефона, я перезвоню в течение 3-х секунд.

VoIP оборудование


ближайшие курсы

10 доводов в пользу Asterisk

Распространяется бесплатно.

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

Безопасен в использовании.

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

Надежен в эксплуатации.

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

Гибкий в настройке.

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

Имеет огромный функционал.

Во многом именно Asterisk показал какой должна быть современная телефонная станция. За многие годы развития функциональность Asterisk расширилась, а все основные возможности по-прежнему доступны бесплатно сразу после установки.

Интегрируется с любыми системами.

То, что Asterisk не умеет сам, он позволяет реализовать за счет интеграции. Это могут быть интеграции с коммерческими телефонными станциями, CRM, ERP системами, биллингом, сервисами колл-трекинга, колл-бэка и модулями статистики и аналитики.

Позволяет телефонизировать офис за считанные часы.

В нашей практике были проекты, реализованные за один рабочий день. Это значит, что утром к нам обращался клиент, а уже через несколько часов он пользовался новой IP-АТС. Безусловно, такая скорость редкость, ведь АТС – инструмент зарабатывания денег для многих компаний и спешка во внедрении не уместна. Но в случае острой необходимости Asterisk готов к быстрому старту.

Отличная масштабируемость.

Очень утомительно постоянно возвращаться к одному и тому же вопросу. Такое часто бывает в случае некачественного исполнения работ или выбора заведомо неподходящего бизнес-решения. С Asterisk точно не будет такой проблемы! Телефонная станция, построенная на Asterisk может быть масштабируема до немыслимых размеров. Главное – правильно подобрать оборудование.

Повышает управляемость бизнеса.

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

Снижает расходы на связь.

Связь между внутренними абонентами IP-АТС бесплатна всегда, независимо от их географического расположения. Также к Asterisk можно подключить любых операторов телефонии, в том числе GSM сим-карты и настроить маршрутизацию вызовов по наиболее выгодному тарифу. Всё это позволяет экономить с первых минут пользования станцией.