Про азы как получить токен и настроить web-push уведомления я уже описал вот в этой статье, посему об этом здесь речи не будет.
Описание начну с javascript, которого мы подключим к странице. Инициализируем FCM:
1 2 3 4 5 6 7 8 9 |
//Настройки FCM //https://console.firebase.google.com/u/1/ var config = { apiKey: "AAAAGtd******************TYcwV6", authDomain: "*******", databaseURL: "https://*****.firebaseio.com", storageBucket: "*******.appspot.com", messagingSenderId: "**************", }; |
Также сразу зададим глобальную переменную для нашего токена:
1 |
var realTokenFCM; |
Запросим у браузера разрешение на показ уведомлений. Дополнительно моя функция запишет учетные данные в IndexedDB, чтобы с ними мог работать сервис воркер (у него нет прямого доступа к кукам). IndexedDB на первый взгляд(особого опыта работы с ней нет) весьма странная штука. Я умудрился заблочить документ транзакцией, что не позволяло даже из браузера удалить созданную базу. Перезапуск не помогал, а вот отключение javascript — вполне. Также у меня были конкретные проблемы при обновлении данных в документе. В общем, чтобы не вдаваться(сейчас) в подробности работы indexedDB наиболее адекватное решение было удалять и создавать заново базу. Т.к. процесс удаления и создания базы происходит только при изменении её версии, то повесил этот параметр к линуксовому времени(epoch). В случае, если indexedDB недоступно — будет произведена запись в firebase напрямую. При работе с базами данных особое внимание уделите завершению транзакций и отладке ошибок. Например, если indexedDB не существует и мы попытаемся его удалить — возникнет ошибка. Важно, чтобы при этом не крашился скрипт. Аналогично при создании базы. Плюс ко всему нельзя совершать две параллельных транзакции с одним документом в firebase. Ну и для записи в firebase не забудьте поставить разброс, а то все клиенты махом в нее ломанутся.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
try { //инициализируем подключение к FCM firebase.initializeApp(config); const fcm_messaging = firebase.messaging(); //запрос на показ Web-PUSH браузеру fcm_messaging.requestPermission() .then(function() { console.log('Уведомления разрешены.'); // Если нотификация разрешена, получаем токен. fcm_messaging.getToken() .then(function(currentToken) { if (currentToken) { realTokenFCM = currentToken; console.info(currentToken); try { var myrand = 1 + Math.random() * (60 + 1 - 1); myrand = Math.floor(myrand); setTimeout(function(){ var indexed_username = getCookieforLocalStore('login'); var indexed_realname = getCookieforLocalStore('displayname').replace("+"," "); var indexed_photo = ('https://**********/search/photo/' + getCookieforLocalStore('login') + '.png'); var indexed_fcm_token = getCookieforLocalStore('FCMtoken'); if((typeof(indexed_username) != "undefined") && (typeof(indexed_photo) != "undefined") && (typeof(indexed_fcm_token) != "undefined") && (indexed_username != "") && (indexed_photo != "") && (indexed_fcm_token != "")){ writeUserData(indexed_username, indexed_realname, indexed_fcm_token, indexed_photo); console.log('Синхронизация выполнена!'); } else { console.error('Не удалось получить куки, проверьте username:'+login+' токен:'+indexed_fcm_token); } }, (5+myrand)*1000); } catch(e){ console.error('Sync Error:' + e); } document.cookie = "FCMtoken=" + currentToken + "; path=/; secure=true"; //отправка токена на сервер SendTokenToServer(currentToken); } else { console.error('No Instance ID token available. Request permission to generate one.'); } }) .catch(function(err) { console.error('An error occurred while retrieving token. ', err); }); // ... }) .catch(function(err) { console.warn('В вашем браузере отключены уведомления.', err); }); try { if(!window.indexedDB){console.warn("Ваш браузер не поддерживат стабильную версию IndexedDB!");} else{ var realtime = new Date(); var dataver = realtime.getTime(); var indexed_username = getCookieforLocalStore('login'); var indexed_realname = getCookieforLocalStore('displayname').replace("+"," "); var indexed_photo = ('***********.by/search/photo/' + getCookieforLocalStore('login') + '.png'); var indexed_fcm_token = getCookieforLocalStore('FCMtoken'); var request1 = indexedDB.open("assistant", dataver); request1.onerror = function(event) { console.warn('Ошибка обновления БД:' + request1.errorCode); }; request1.onsuccess = function(event) { console.log('Выполнено обновление БД:' + request1.result); request1.result.close(); }; request1.onupgradeneeded = function(event) { var db = event.target.result; db.deleteObjectStore("ServiceWorker"); console.log('Хранилище удалено.'); }; dataver++; var request2 = indexedDB.open("assistant", dataver); request2.onerror = function(event) { console.warn('Ошибка обновления БД:' + request2.errorCode); }; request2.onsuccess = function(event) { console.log('Выполнено обновление БД:' + request2.result); request2.result.close(); }; request2.onupgradeneeded = function(event) { var db = event.target.result; var objectStore = db.createObjectStore("ServiceWorker", { keyPath: "username" }); objectStore.createIndex("username", "username", { unique: true }); objectStore.createIndex("realname", "realname", { unique: false }); objectStore.createIndex("photo", "photo", { unique: false }); objectStore.createIndex("fcm_token", "fcm_token", { unique: true }); if((typeof(indexed_username) != "undefined") && (typeof(indexed_photo) != "undefined") && (typeof(indexed_fcm_token) != "undefined") && (typeof(indexed_realname) != "undefined") && (indexed_username != "") && (indexed_photo != "") && (indexed_fcm_token != "") && (indexed_realname != "")){ objectStore.put({ username: indexed_username, realname: indexed_realname, photo: indexed_photo, fcm_token: indexed_fcm_token }); console.log('Хранилище создано!'); } else { console.warn('Не удалось получить куки, проверьте username:'+login+' токен:'+indexed_fcm_token); } }; } } catch (e) { console.warn('У вас проблемы с синхронизацией, пробуем второй вариант. Ошибка:' + e); try { var indexed_username = getCookieforLocalStore('login'); var indexed_realname = getCookieforLocalStore('displayname').replace("+"," "); var indexed_photo = ('https://***********.by/search/photo/' + getCookieforLocalStore('login') + '.png'); var indexed_fcm_token = getCookieforLocalStore('FCMtoken'); if((typeof(indexed_username) != "undefined") && (typeof(indexed_photo) != "undefined") && (typeof(indexed_fcm_token) != "undefined") && (indexed_username != "") && (indexed_photo != "") && (indexed_fcm_token != "")){ writeUserData(indexed_username, indexed_realname, indexed_fcm_token, indexed_photo); console.log('Синхронизация выполнена!'); }else { console.warn('Не удалось получить куки, проверьте username:'+login+' токен:'+indexed_fcm_token); } } catch(e){ console.error('Sync Error:' + e); } } function writeUserData(username, realname, fcm_token, image_url) { try { var tokendata = new Date(); tokendatastr = tokendata.toString(); firebase.database().ref('users/' + username.replace(".","_")).update({ email: username+'@****.by', username: realname, token: fcm_token, profile_picture : image_url, time: tokendatastr }); } catch(e) { console.log('Send to FCM Error:' + e); } } } catch(e) { console.error('Initialize firebase Error:' + e); } |
Для получение данных из куков используем функцию:
1 2 3 4 5 6 7 8 9 10 11 |
function getCookieforLocalStore(name) { try { var matches = document.cookie.match(new RegExp( "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)" )); return matches ? decodeURIComponent(matches[1]) : undefined; } catch(e) { console.error('Load Cookie Error:' + e); return undefined; } } |
Запись токенов в MySQL осуществляется со стороны javascript функцией:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function SendTokenToServer(currentToken) { try { xmlhttp=new XMLHttpRequest(); xmlhttp.onreadystatechange=function() { if (this.readyState==4 && this.status==200) { if(this.responseText == 'token has been update'){ console.log('Токен успешно обновлен в БД!'); } else { console.log(this.responseText); } } } xmlhttp.open("POST","savetoken.php?securitykey=" + encodeURIComponent("*********"),true); xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xmlhttp.send("token=" + currentToken); } catch(e){ console.error('Send token to server Error:' + e); } } |
Со стороны бэкэнда(php) скриптом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?php session_start(); header("Content-type: text/txt; charset=UTF-8"); if((urldecode($_GET['securitykey']) != '***********') || !isset($_SESSION['login'])){ echo 'not valid request'; exit; } include('settings.php'); $login = $_SESSION['login']; $mysqli_user = new mysqli($assist_db_ip, $assist_db_user, $assist_db_pass, $assist_db_name); if ($mysqli_user->connect_errno) { echo "Не удалось подключиться к MySQL:".$mysqli_user->connect_error; exit; } $mysqli_user->query("SET NAMES utf8"); $mysqli_user->query("update `general.user_settings` set fcm_token = '".$_POST['token']."' where LOGIN like '$login'"); echo 'token has been update'; $mysqli_user->close(); exit; ?> |
Также решил добавить функцию отправки уведомлений прямо из javascript (без обращению к бэкэнду):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
function assist_push(assist_sender, assist_receiver, assist_message){ try { firebase.database().ref('/users/'+assist_sender.replace(".","_")).once('value').then(function(snapshot) { if((typeof(snapshot.val().username) != "undefined") && (snapshot.val().username != "")){ var assist_title = snapshot.val().username; } else{ var assist_title = ''; } if((typeof(snapshot.val().profile_picture) != "undefined") && (snapshot.val().profile_picture != "")){ var assist_icon = snapshot.val().profile_picture; } else{ var assist_icon = ''; } firebase.database().ref('/users/'+assist_receiver.replace(".","_")).once('value').then(function(snapshot) { if((typeof(snapshot.val().token) != "undefined") && (snapshot.val().token != "")){ var assist_token = snapshot.val().token; var assist_body = assist_message; try { if((typeof(assist_title) != "undefined") && (assist_title != "") && (typeof(assist_body) != "undefined") && (assist_body != "") && (typeof(assist_icon) != "undefined") && (assist_icon != "") && (typeof(assist_token) != "undefined") && (assist_token != "")) { var testbody = '{"notification":{"title":"' + assist_title + '","body":"' + assist_body + '","icon":"' + assist_icon + '","tag":"Assistant_Notify"}, "priority":"normal", "registration_ids":["' + assist_token + '"]}'; xmlhttp=new XMLHttpRequest(); xmlhttp.onreadystatechange=function() { if (this.readyState==4 && this.status==200) { console.log(this.responseText); } } xmlhttp.open("POST","https://fcm.googleapis.com/fcm/send",true); xmlhttp.setRequestHeader('Authorization', 'key='+config.apiKey); xmlhttp.setRequestHeader('Content-Type', 'application/json'); xmlhttp.send(testbody); } else { console.log('Не корректные аргументы функции!'); } } catch(e){ console.error(e); } } }); }); }catch(e){ console.error(e); } } |
На этом наш скрипт готов. Можно приступать к написанию service worker. Он должен лежать в корне сайта и иметь имя firebase-messaging-sw.js. Инициализируем приложение и базу данных:
1 2 3 4 5 6 7 8 9 10 |
importScripts('https://www.gstatic.com/firebasejs/4.2.0/firebase.js'); firebase.initializeApp({ apiKey: "AAAA**************TYcwV6", authDomain: "******.farmin.by", databaseURL: "https://*******.firebaseio.com", storageBucket: "********.appspot.com", messagingSenderId: "**********" }); var database = firebase.database(); |
Опишем событие установки service worker, если необходимо. Если нет, можно этот пункт опустить:
1 2 3 4 5 |
//Выполняем инсталяцию приложения self.addEventListener('install', function(event) { event.waitUntil(skipWaiting()); console.info('Установка fcm service worker выполнена!'); }); |
Аналогично с событием активации service worker(в смысле можно опустить). Я не опускал, т.к. у меня здесь происходит дополнительная синхронизация с firebase:
1 2 3 4 5 6 |
//Активируем приложения для данной вкладки self.addEventListener('activate', function(event) { event.waitUntil(clients.claim()); console.info('Активация fcm service worker выполнена!'); MySyncFunc(); }); |
Собственна сама функция синхронизации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//Синхронизация indexedDB с FCM БД function MySyncFunc(){ try{ db_transaction = self.indexedDB.open('assistant'); db_transaction.onsuccess = function(e) { db_transaction_inc = db_transaction.result.transaction(['ServiceWorker'], "readonly").objectStore("ServiceWorker").getAll(); db_transaction_inc.onsuccess = function(e) { db_transaction_inc2 = db_transaction_inc.result[0]; writeUserData(db_transaction_inc2.username, db_transaction_inc2.realname, db_transaction_inc2.fcm_token, db_transaction_inc2.photo); db_transaction.result.close(); } } console.log('Синхронизация выполнена!'); } catch(e) { console.error('Sync Error:' + e); } } |
Как видно, она обращается к другой функции записи в firebase:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//Регистрация юзера в FCM БД function writeUserData(username, realname, fcm_token, image_url) { try { var tokendata = new Date(); tokendatastr = tokendata.toString(); firebase.database().ref('users/' + username.replace(".","_")).update({ email: username+'@farmin.by', username: realname, token: fcm_token, profile_picture : image_url, time: tokendatastr }); } catch(e) { console.error('Send to FCM Error:' + e); } } |
В своей статье о настройках прав и авторизации в firebase я вести не буду, об этом когда-нибудь напишу отдельно. Это того заслуживает.
Событие получение push от сервера firebase cloud messaging. В моем случае они фильтруются по тэгу и вызывают соответствующую функцию. Здесь важно вернуть в промис event.waitUntil промис showNotification, иначе помимо вашего пуша, будет показываться и стандартный «сайт был обновлен в фоновом режиме» или т.п.:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//Получаем сообщение в сервис-воркер self.addEventListener('push', function(event) { event.waitUntil( self.registration.pushManager.getSubscription().then(function(subscription) { //console.info(event.data.json()); if(event.data.json().notification.tag == 'Asterisk_Incomming'){ if(typeof(event.data.json().data) == "undefined"){ return NotifyIncomming(event.data.json()); }else { return NotifyIncommingCall(event.data.json()); } }else if(event.data.json().notification.tag == 'Asterisk_Queue'){ return NotifyIncomming(event.data.json()); }else if(event.data.json().notification.tag == 'Assistant_Notify'){ return NotifyIncomming(event.data.json()); }else if((event.data.json().notification.tag == 'Helpdesk_Notify') || (event.data.json().notification.tag == 'Helpdesk_Notify_Tech')){ return NotifyHelpdesk(event.data.json()); }else{ return NotifyIncomming(event.data.json()); } }) .catch(function(err) { console.error('Невозможно получить данные с сервера: ', err); }) ); }); |
Опишем обработку событий клика по уведомлению, я разделил в action знаком | команду и аргумент. При клике на кнопку — обрабатываю событие, при клике на само уведомление — закрываю его:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//Клик по окну Notification self.addEventListener('notificationclick', function(event) { if(typeof(event.action) != "undefined" && event.action !== ""){ clik_behavior = event.action.split('|'); if(clik_behavior[0] == "busy_call"){ PostMessage(clik_behavior[1], ""); }else if((clik_behavior[0] == "setting_call") || (clik_behavior[0] == "helpdesk_view") || (clik_behavior[0] == "helpdesk_pick_up")){ clients.openWindow(clik_behavior[1]); } } else{ myNotifyClose(event.notification.tag); } }); |
Событие закрытия окна уведомлений, если не нужно — можно опустить:
1 2 3 4 |
//Закрытие окна уведомления self.addEventListener('notificationclose', function(event) { console.log('notificationclose fcm service worker'); }); |
Функция создания стандартного окна уведомления, переданные данные хранятся в data.json().notification и data.json().data:
1 2 3 4 5 6 7 8 9 |
//Функция создания стандартного окна уведомления function NotifyIncomming(data){ return self.registration.showNotification(data.notification.title, { body : data.notification.body, icon : data.notification.icon, tag: data.notification.tag } ); } |
Теперь создадим нестандартное окно уведомлений, с массивом actions. POST-запрос уходит в бэкэнд по клику на соответствующую кнопку. В данном случае скрипт на php подключается к Asterisk AMI и рвет заданный канал связи:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//Функция создания окна уведомления о входящем вызове(+ кнопки) function NotifyIncommingCall(data){ return self.registration.showNotification(data.notification.title, { body : data.notification.body, icon : data.notification.icon, tag: data.notification.tag, actions: [ { icon: "images/menu/call_noanswer.png",//сылка на иконку в кнопке action: "busy_call|/search/ami_asterisk.php?hangup&channel=" + data.data.channel, //наименивание действия title: "Отклонить" //лейбл } ] } ); } |
Или с двумя кнопками массива actions(заберет заявку в ServiceDesk или просто откроет):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
function NotifyHelpdesk(data){ if(data.notification.tag == 'Helpdesk_Notify'){ return self.registration.showNotification(data.notification.title, { body : data.notification.body, icon : data.notification.icon, tag: data.notification.tag, actions: [ { icon: "images/menu/helpdesk_view.png",//сылка на иконку в кнопке action: "helpdesk_view|http://helpdesk.farmin.by/WorkOrder.do?woMode=viewWO&woID=" + data.data.channel, //наименивание действия title: "Перейти к заявке" //лейбл } ] } ); }else if(data.notification.tag == 'Helpdesk_Notify_Tech'){ return self.registration.showNotification(data.notification.title, { body : data.notification.body, icon : data.notification.icon, tag: data.notification.tag, actions: [ { icon: "images/menu/helpdesk_pick_up.png",//сылка на иконку в кнопке action: "helpdesk_pick_up|http://helpdesk.farmin.by/AssignOwner.do?pickup=true&woID=" + data.data.channel, //наименивание действия title: "Забрать заявку" //лейбл }, { icon: "images/menu/helpdesk_view.png",//сылка на иконку в кнопке action: "helpdesk_view|http://helpdesk.farmin.by/WorkOrder.do?woMode=viewWO&woID=" + data.data.channel, //наименивание действия title: "Перейти к заявке" //лейбл } ] } ); } } |
Для отправки POST-запроса(можно и GET отправлять) использую объект Request в service worker:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//Функция отправки POST-запроса(можно юзать и для GET) function PostMessage(addr, argument){ var myHeaders = new Headers();myHeaders.append('Content-Type', 'application/x-www-form-urlencoded'); var channel = 'channel=' + argument; const myRequest = new Request(addr, {method: 'POST', headers: myHeaders, body: channel}); fetch(myRequest).then(response => { if (response.status === 200) { myNotifyClose('Asterisk_Incomming'); } else { throw new Error('Something went wrong on api server!'); } }).then(response => { console.debug(response); }).catch(error => { console.error(error); }); } |
Функция закрытия уведомления по тэгу + закрывает системное уведомление(сайт был обновлен в фоне), если оно есть:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//Функция удаления уведомления function myNotifyClose(tag){ try { self.registration.getNotifications().then((resolve, reject) => { for (var i = 0; i < resolve.length; i++) { if((resolve[i].tag == tag) || (resolve[i].tag == 'user_visible_auto_notification')) resolve[i].close(); } }); } catch(e) { console.error('Close Notification Error:' + e); } } |
Пара скринов:
Собственно сервер и клиент готовы, теперь осталось реализовать сервер-серверную часть. Разберем на примере работы АТС. Можно слать уведомления напрямую из Asterisk, но у меня была еще цель слать уведомления о звонке в отдел, потому сделал веб-хук на бэкэнде:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
<?php /* формат входящих сообщений: { "vnum": [ "571" ], "name": "Тест", "body": "кликните", "type": "incoming_call", "location": "office", "securitykey": "**********" } Параметры: "vnum" - массив внутренних номеров* "login" - массив логинов, если первый аргумент "all", то отправит всем* "name" - титл уведомления "body" - тело уведомления "type" - тип уведомления** "location" - местоположение юзера, не актуально если задан массив login "securitykey" - обязательный ключ, для обращения к скрипту * обязательный аргумент для отбора токенов массив login или vnum, причем login в приоритете ** на данный момент реализован тип incoming_call */ include 'settings.php'; //настройки БД $content = file_get_contents("php://input"); //получаем json if(!isset($content)){ //проверяем, что данные получены echo 'not valid request'; exit; } $input_json = json_decode($content, TRUE); $securitykey = $input_json['securitykey']; if($securitykey != '****************'){ //проверяем ключ API echo 'not valid request'; exit; } $login_arr = $input_json['login']; $vnum_arr = $input_json['vnum']; $db_connect = new mysqli($assist_db_ip, $assist_db_user, $assist_db_pass, $assist_db_name); if ($db_connect->connect_errno) { header("Content-type: text/txt; charset=UTF-8"); echo "Не удалось подключиться к MySQL:".$db_connect->connect_error; exit; } $db_connect->query("SET NAMES utf8"); if(isset($login_arr)){ //если получен массив {"login":[...]} if($login_arr[0] != 'all'){ //если не "login":["all"], выбираем конкретных пользователей $i=0; for($j=0;$j<count($login_arr);$j++){ $login = $login_arr[$j]; $arr = $db_connect->query("SELECT `LOGIN`,`VNUM`, `fcm_token` FROM `general.user_settings` WHERE LOGIN = '$login' AND `fcm_token` IS NOT NULL ORDER BY Id"); while( $row = $arr->fetch_assoc() ){ $token[$i] = $row['fcm_token']; $q_vnum[$i] = $row['VNUM']; $i++; } $arr->close(); } } else { //иначе выбираем всех пользователей $arr = $db_connect->query("SELECT `LOGIN`,`VNUM`, `fcm_token` FROM `general.user_settings` WHERE `fcm_token` IS NOT NULL ORDER BY Id"); $i=0; while( $row = $arr->fetch_assoc() ){ $token[$i] = $row['fcm_token']; $q_vnum[$i] = $row['VNUM']; $i++; } $arr->close(); } } elseif(isset($vnum_arr) && isset($input_json['location'])) { //иначе проверяем, что получен массив {"vnum":[...]} $i=0; for($j=0;$j<count($vnum_arr);$j++){ if($input_json['location'] == 'office') { $vnum = $vnum_arr[$j]; $arr = $db_connect->query("SELECT `general.user_settings`.`LOGIN`, `general.user_settings`.`VNUM`, `general.user_settings`.`fcm_token` FROM `general.user_settings` WHERE (`general.user_settings`.`LOGIN` IN (SELECT `search.farmin`.`LOGIN` FROM `search.farmin` WHERE `search.farmin`.`VNUM` LIKE '$vnum' AND `search.farmin`.`TYPE` LIKE 'office') OR `general.user_settings`.`LOGIN` IN (SELECT `search.holding`.`LOGIN` FROM `search.holding` WHERE `search.holding`.`VNUM` LIKE '$vnum') OR `general.user_settings`.`VNUM` LIKE '$vnum') AND `general.user_settings`.`fcm_token` IS NOT NULL ORDER BY Id"); while( $row = $arr->fetch_assoc() ){ $token[$i] = $row['fcm_token']; $q_vnum[$i] = $row['VNUM']; $i++; } $arr->close(); } elseif($input_json['location'] == 'sklad'){ $vnum = $vnum_arr[$j]; $arr = $db_connect->query("SELECT `general.user_settings`.`LOGIN`, `general.user_settings`.`fcm_token` FROM `general.user_settings` WHERE (`general.user_settings`.`LOGIN` IN (SELECT `search.farmin`.`LOGIN` FROM `search.farmin` WHERE `search.farmin`.`VNUM` LIKE '$vnum' AND `search.farmin`.`TYPE` LIKE 'sklad') OR `general.user_settings`.`VNUM` LIKE 's".$vnum."') AND `general.user_settings`.`fcm_token` IS NOT NULL ORDER BY Id"); while( $row = $arr->fetch_assoc() ){ $token[$i] = $row['fcm_token']; $i++; } $arr->close(); } } } else { //иначе выкидываем echo 'not valid request'; $db_connect->close(); exit; } if($input_json['type'] == 'incoming_call'){ if(!isset($q_vnum)){ $q_vnum = $vnum_arr; //если номер для очереди не найден, пытаемся получить его из json(актуально, если не задан токен) } $image = 'incoming_call.jpg'; if($input_json['location'] == 'sklad'){ goto noqueue; } //скрипт для отправки уведомления о звонке в отдел $image2 = 'incoming_queue.jpg'; $queue = new mysqli($assist_db_ip, $assist_db_user, $assist_db_pass, $assist_db_ats); if ($queue->connect_errno) { header("Content-type: text/txt; charset=UTF-8"); echo "Не удалось подключиться к MySQL:".$queue->connect_error; exit; } $queue->query("SET NAMES utf8"); $g=0; if ($result = $queue->query('SELECT * FROM `queue` ORDER BY name')) { //получаем все очереди while( $row = $result->fetch_assoc() ){ $queues[$g] = explode(",", $row['agents']); $g++; } $result->close(); //освобождаем память } $queue->close(); $m = 0; for($g=0;$g<count($q_vnum);$g++){ if(isset($queues_new)){unset($queues_new);} for($i=0;$i<count($queues);$i++){ //ищем полученный номер в списке очередей if(in_array($q_vnum[$g], $queues[$i])){ $queues_new = $queues[$i]; } } while ($t_vnum = current($queues_new)) { //исключаем наш номер из списка, чтобы уведомление не задваивалось if ($t_vnum == $q_vnum[$g]) { unset($queues_new[key($queues_new)]); sort($queues_new); break; } next($queues_new); } if(isset($queues_new)){ $str ='`search.farmin`.`VNUM` LIKE \''.$queues_new[0].'\''; //создаем запрос для выбора токенов for($i=1;$i<count($queues_new);$i++){ $str .= ' OR `search.farmin`.`VNUM` LIKE \''.$queues_new[$i].'\''; } $arr = $db_connect->query("SELECT `general.user_settings`.`LOGIN`, `general.user_settings`.`VNUM`, `general.user_settings`.`fcm_token` FROM `general.user_settings` WHERE `general.user_settings`.`LOGIN` IN (SELECT `search.farmin`.`LOGIN` FROM `search.farmin` WHERE (".$str.") AND `search.farmin`.`TYPE` LIKE 'office') AND `general.user_settings`.`fcm_token` IS NOT NULL ORDER BY Id"); while( $row = $arr->fetch_assoc() ){ //получаем токены для отправки уведомления в отдел $token2[$m] = $row['fcm_token']; $m++; } $arr->close(); } } noqueue: $db_connect->close(); if(isset($token)){ //обычная отправка уведомлений $url = 'https://fcm.googleapis.com/fcm/send'; $YOUR_API_KEY = 'AAAAGt**********************************TYcwV6'; // Server key if (count($token)> 1){ $YOUR_TOKEN_ID = array(); for($i=0;$i<count($token);$i++){ array_push($YOUR_TOKEN_ID,$token[$i]); // Client token id } //var_dump($YOUR_TOKEN_ID); //https://firebase.google.com/docs/cloud-messaging/http-server-ref $request_body = [ 'registration_ids' => $YOUR_TOKEN_ID, 'priority' => 'high', //приоритет high или normal 'notification' => [ 'title' => $input_json['name'], //название уведомления 'body' => $input_json['body'], //тело уведомления 'icon' => 'images/'.$image, //картинка уведомления 'tag' => 'Asterisk_Incomming' //тэг уведомления, уведомления с одним тегом заменяют друг друга ], 'data' => [ 'channel' => $input_json['channel'] ], 'time_to_live' => 10, //максимальное время жизни уведомления на сервере FCM в сек ]; } else { $request_body = [ 'to' => $token[0], 'priority' => 'high', //приоритет high или normal 'notification' => [ 'title' => $input_json['name'], //название уведомления 'body' => $input_json['body'], //тело уведомления 'icon' => 'images/'.$image, //картинка уведомления 'tag' => 'Asterisk_Incomming' //тэг уведомления, уведомления с одним тегом заменяют друг друга ], 'data' => [ 'channel' => $input_json['channel'] ], 'time_to_live' => 10, //максимальное время жизни уведомления на сервере FCM в сек ]; } $fields = json_encode($request_body); $request_headers = [ 'Content-Type: application/json', 'Authorization: key=' . $YOUR_API_KEY, ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers); curl_setopt($ch, CURLOPT_POSTFIELDS, $fields); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $response .= curl_exec($ch).PHP_EOL; curl_close($ch); echo $response; } if(isset($token2)){ //отправка уведомлений в отдел $url = 'https://fcm.googleapis.com/fcm/send'; $YOUR_API_KEY = 'AAAAGtd****************************CVTYcwV6'; // Server key if (count($token2)> 1){ $YOUR_TOKEN_ID = array(); for($i=0;$i<count($token2);$i++){ array_push($YOUR_TOKEN_ID,$token2[$i]); // Client token id } //var_dump($YOUR_TOKEN_ID); //https://firebase.google.com/docs/cloud-messaging/http-server-ref $request_body = [ 'registration_ids' => $YOUR_TOKEN_ID, 'priority' => 'high', //приоритет high или normal 'notification' => [ 'title' => 'Звонок в отдел', //название уведомления 'body' => $input_json['name'] . PHP_EOL . $input_json['body'], //тело уведомления 'icon' => 'images/'.$image2, //картинка уведомления 'tag' => 'Asterisk_Queue' //тэг уведомления, уведомления с одним тегом заменяют друг друга ], 'time_to_live' => 10, //максимальное время жизни уведомления на сервере FCM в сек ]; } else { $request_body = [ 'to' => $token2[0], 'priority' => 'high', //приоритет high или normal 'notification' => [ 'title' => 'Звонок в отдел', //название уведомления 'body' => $input_json['name'] . PHP_EOL . $input_json['body'], //тело уведомления 'icon' => 'images/'.$image2, //картинка уведомления 'tag' => 'Asterisk_Queue' //тэг уведомления, уведомления с одним тегом заменяют друг друга ], 'time_to_live' => 10, //максимальное время жизни уведомления на сервере FCM в сек ]; } $fields = json_encode($request_body); $request_headers = [ 'Content-Type: application/json', 'Authorization: key=' . $YOUR_API_KEY, ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); curl_setopt($ch, CURLOPT_HTTPHEADER, $request_headers); curl_setopt($ch, CURLOPT_POSTFIELDS, $fields); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $response .= curl_exec($ch).PHP_EOL; curl_close($ch); echo $response; } } else { echo 'тестовый режим'; $db_connect->close(); } exit; ?> |
Для того, чтобы активировать веб-хук добавим в диалплан Asterisk в любой из моих макросов, исполняемых перед вызовом локального абонента строчку:
1 |
exten => s,n(notelegramm),SYSTEM(curl -k -H "Content-Type: application/json" -X POST -d '{"vnum":["${ARG1}"],"name":"${CALLERID(name)}","body":"${CALLERID(num)}","type":"incoming_call","location":"office","channel": "${CHANNEL}","securitykey": "**********"}' https://assistant.farmin.by/sendpush.php) |
Или же вариант целиком макроса, нужно включить в экстеншн:
1 2 3 4 5 6 7 |
[macro-assistant-farmin-by] exten => s,1,NoOp(Номер ${ARG3} вызывает ${ARG2} < ${ARG1} > ) exten => s,n, Set(ARG2=${IF($[ ${ARG2:0:4} = 1000]?Входящий вызов:${ARG2})}) exten => s,n, Set(ARG1=${IF($[ ${ARG1:0:4} = 1000]?номер не определен:${ARG1})}) exten => s,n,SYSTEM(curl -k -H "Content-Type: application/json" -X POST -d '{"vnum":["${ARG3}"],"name":"${ARG2}","body":"${ARG1}","type":"incoming_call","location":"sklad","securitykey": "*******"}' https://assistant.farmin.by/sendpush.php) exten => s,n,MacroExit() exten => h,1,Hangup() |
Очередность выполнения макросов и экстеншнов можно посмотреть, подключившись к консоли Asterisk командой:
1 |
asterisk -vvvvvr |
.
Ну и собственно скрипт для работы с Asterisk AMI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<?php $strhost = "****************"; $strport = "************"; $timeout = "10"; /*$num=$_REQUEST['num']; $cid=$_REQUEST['cid']; $c=$_REQUEST['c']; $p=$_REQUEST['p'];*/ $errno=0 ; $errstr=0 ; $sconn = fsockopen ($strhost, $strport, $errno, $errstr, $timeout); if(!$sconn) { echo "Connection to $strhost:$strport failed"; exit; } fputs ($sconn, "Action: login\r\n"); fputs ($sconn, "Username: *******\r\n"); fputs ($sconn, "Secret: **************\r\n"); fputs ($sconn, "Events: off\r\n\r\n"); fgets($sconn,1000000); usleep(500); $channel = $_GET['channel']; if(isset($_GET['hangup'])) { echo 'Вызов отклонен'; fputs ($sconn, "Action: Hangup\r\n"); fputs ($sconn, "Channel: $channel\r\n"); } fgets($sconn,32768); fputs ($sconn, "Async: yes\r\n\r\n" ); fputs ($sconn, "Action: Logoff\r\n\r\n"); fgets($sconn,32768); usleep (500); fclose ($sconn); ?> |
Пример использования (видео подтупливает, т.к. снято с удаленного ПК):