Skip to content

API, используемое мобильными приложениями для iOS и Android / API for customers' mobile applications

Notifications You must be signed in to change notification settings

rosteleset/ApplicationAPI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ApplicationAPI

Данный API используют наши мобильные приложения для iOS и Android

Для просмотра API вы можете перейти по данной ссылке либо клонировать этот репозиторий себе.

API сгенерировано автоматически при помощи apidoc

Пример, который поможет вам начать создавать собственный движок для домофонии

Ниже наброски того, как сделать мобильное приложение на базе исходников Linphone, которое будет принимать звонки с вашего Asterisk через push-notifications. Данный пример иллюстрирует наш подход, при помощи которого мы делаем интеграцию между SIP-домофонами <-> Asterisk <-> Мобильными приложениями

Firebase (google)

  1. регистрируемся в google firebase (https://firebase.google.com/)

  2. потребуется зарегистрировать проект и приложение

0001

0002

потребуется придумать имя приложения

0003

и отключить (а можно и оставить) аналитику

0004

проект готов, надо добавить приложение

0005

0006

0007

  1. надо скачать google-services.json

  2. теперь надо создать сервисный аккаунт

0008

0009

и сохранить его ключ в файл serviceAccountKey.json

Android

  1. скачиваем linphone

      git clone https://github.com/BelledonneCommunications/linphone-android
    
  2. файл google-services.json (см выше) надо положить в linphone-android/app (там уже есть от разработчиков linphone, просто перезаписать)

  3. в файле google-services.json в секции "client" дублируем содержимое и во втором экземпляре к package_name добавляем .debug

0012

  1. в linphone-android/app/build.gradle меняем "org.linphone" на имя которое придумали при создании приложения в firebase

  2. генерируем ключи для подписи приложения

      keytool -genkey -keystore linphone-android/app/bc-android.keystore -storepass lp1234 -alias lp1234 -keypass lp1234 -dname "CN=Mikhail Ivanov O=StartAndroid C=RU" -validity 10000 -keyalg RSA -keysize 2048
    
  3. подставляем lp1234 в linphone-android/keystore.properties

0014

  1. качаем и ставим Android Studio IDE https://developer.android.com/studio#downloads (если еще нет)

  2. открываем проект linphone-android

  3. компилируем, запускаем на устройстве или эмуляторе

0015

  1. в настройках приложения включаем пуши, ОТКЛЮЧАЕМ автозапуск и фоновую службу

0010

0011

с андроидом все (релиз, ребрендинг и т.п. - уже сами :))

iOS

всё аналогично Android:

  1. скачиваем https://github.com/BelledonneCommunications/linphone-iphone
  2. заменяем название приложения, версию и данные разработчиков
  3. Необходимые ключи, провижининг профайлы и сертификаты для подписи приложения XCode должен создать автоматически, либо их можно добавить их вручную из панели разработчика на developer.apple.com
  4. заменяем GoogleService-Info.plist на свой, взятый из нашего аккаунта Firebase
  5. собираем проект, архивируем, отправляем в AppStore на проверку, публикуем.

Asterisk

  1. диалплан на lua (надо подождать пока не появится extension)
local timeout = os.time() + 60

local pjsip_extension = ''

push(extension, channel.CALLERID("name"):get(), channel.CALLERID("num"):get())

log_debug("starting loop for: "..extension)
while os.time() < timeout do
   pjsip_extension = channel.PJSIP_DIAL_CONTACTS(extension):get()
   if pjsip_extension ~= "" then
      log_debug("has registration: "..extension.." ["..pjsip_extension.."]")
      app.Dial(pjsip_extension, 60, 'mtT')
   else
      app.Wait(0.5)
   end
end
  1. надо как-то получать токены пушей и отправлять их, для этого потребуется небольшой скрипт на node.js (прилагается)

  2. рядом с lp.js надо положить serviceAccountKey.json

  3. скрипту потребуется AMI

  4. и некоторое количество зависимостей npm i firebase-admin asterisk-manager express redis

  5. ключи будем хранить в redis apt-get install redis

  6. у extension желательно поставить транспорт tcp, тогда при отключении телефона астериск сразу поймет что контакт отвалился

Исходный код

  1. lp.js (регистрация токенов, отправка пушей)
#!/usr/bin/nodejs

const database = 'https://presentation-voxlink.firebaseio.com';

const fs = require('fs');
const path = require('path');
const admin = require('firebase-admin');
const ami = new require('asterisk-manager')('5038', '127.0.0.1', 'asterisk', '881d6256664648e0ebe1ed0e9b1340f2', true);
const app = require('express')();
const redis = require("redis").createClient();

admin.initializeApp({
    credential: admin.credential.cert(require(path.join(__dirname, 'serviceAccountKey.json'))),
    databaseURL: database,
});

ami.keepConnected();

app.use(require('body-parser').urlencoded({ extended: true }));
app.listen(8082, '127.0.0.1');

function pushOk(token, result) {
    if (result && result.successCount && parseInt(result.successCount)) {
        console.log((new Date()).toLocaleString() + " ok: " + token);
    } else {
        pushFail(token, result);
    }
}

function pushFail(token, error) {
    console.log((new Date()).toLocaleString() + " err: " + token);

    let broken = false;
    if (error && error.results && error.results.length && error.results[0] && error.results[0].error && error.results[0].error.code) {
        if (error.results[0].error.code == 'messaging/registration-token-not-registered') {
            for (let i in contacts) {
                if (contacts[i] == token) {
                    delete contacts[i];
                    redis.set('contacts', JSON.stringify(contacts));
                }
            }
            broken = true;
        }
    }

    if (!broken) {
        fs.appendFileSync('/tmp/pushFail.log', (new Date()).toLocaleString() + " err: " + token + "\n" + JSON.stringify(error) + "\n\n");
    }
}

function realPush(msg, data, options, token, type) {

    console.log(token, type, msg, data, options);

    if (parseInt(type) === 0) {

        let message = {
            notification: msg,
            data: data,
        };

        if (options) {
            admin.messaging().sendToDevice(token, message, options).then(r => {
                pushOk(token, r);
            }).catch(e => {
                pushFail(token, e);
            });
        } else {
            admin.messaging().sendToDevice(token, message).then(r => {
                pushOk(token, r);
            }).catch(e => {
                pushFail(token, e);
            });
        }
    } else {

        let http2_server = (parseInt(type) === 2)?'https://api.sandbox.push.apple.com':'https://api.push.apple.com';

        let curl = new Curl();

        curl.setOpt(Curl.option.HTTP_VERSION, 3);
        curl.setOpt(Curl.option.URL, `${http2_server}/3/device/${token}`);
        curl.setOpt(Curl.option.PORT, 443);
        curl.setOpt(Curl.option.HTTPHEADER, [
            `apns-topic: ${app_bundle_id}.voip`,
            `apns-push-type: voip`,
            `User-Agent: ${app_bundle_id}`,
        ]);
        curl.setOpt(Curl.option.POST, true);
        curl.setOpt(Curl.option.POSTFIELDS, JSON.stringify({
            data: data,
        }));
        curl.setOpt(Curl.option.TIMEOUT, 30);
        curl.setOpt(Curl.option.SSL_VERIFYPEER, false);
        curl.setOpt(Curl.option.SSLCERT, cert);
        curl.setOpt(Curl.option.HEADER, true);
        curl.setOpt(Curl.option.VERBOSE, false);

        curl.on('end', code => {
            if (parseInt(code) === 200) {
                pushOk(token, { successCount: 1 });
            } else {
                pushFail(token, { errorCode: code });
            }
            curl.close();
        });

        curl.on('error', () => {
            curl.close();
        });

        curl.perform();
    }
}

var contacts = {};

ami.on('contactstatus', e => {
    if (e.aor) {
        let uri = e.uri.split(';');
        let token;
        let type;
        for (let i = 0; i < uri.length; i++) {
            let p = uri[i].split('=');
            switch (p[0]) {
                case 'pn-tok':
                    token = p[1];
                    break;
                case 'pn-type':
                    type = p[1];
                    break;
            }
        }
        if (token && type == 'firebase') {
            contacts[e.aor] = token;
            console.log(e.aor, token);
            redis.set('contacts', JSON.stringify(contacts));
        }
    }
});

app.get('/wakeup', function (req, res) {
    console.log(req.query);
    if (req.query.ext && contacts[req.query.ext]) {
        realPush({
                // empty message
            }, {
                type: 'voip',
                realm: req.query.realm?req.query.realm:'Unknown',
                user: req.query.from?req.query.from:'Unknown',
            }, {
                priority: 'high',
                mutableContent: true,
            },
            contacts[req.query.ext],
            0
        );
    }
    res.status(204).send();
});

redis.get('contacts', (e, v) => {
    if (!e && v) {
        contacts = JSON.parse(v);
        console.log(contacts);
    }
});
  1. extension.lua (диалплан) apt-get install lua-socket
http = require 'socket.http'

function char_to_pchar(c)
   return string.format("%%%02X", c:byte(1, 1))
end

function encodeURI(str)
   return (str:gsub("[^%;%,%/%?%:%@%&%=%+%$%w%-%_%.%!%~%*%'%(%)%#]", char_to_pchar))
end

function encodeURIComponent(str)
   return (str:gsub("[^%w%-_%.%!%~%*%'%(%)]", char_to_pchar))
end

function push(ext, realm, from)
   http.request("http://127.0.0.1:8082/wakeup?ext="..encodeURIComponent(tostring(ext)).."&realm="..encodeURIComponent(tostring(realm)).."&from="..encodeURIComponent(tostring(from)))
end

extensions = {
   [ "default" ] = {

      [ "_." ] = function ()
         app.Answer()
         app.MusicOnHold()
      end,

      [ "_XXXX" ] = function (context, extension)
         local timeout = os.time() + 60

         local pjsip_extension = ''

         push(extension, channel.CALLERID("name"):get(), channel.CALLERID("num"):get())

         while os.time() < timeout do
            pjsip_extension = channel.PJSIP_DIAL_CONTACTS(extension):get()
            if pjsip_extension ~= "" then
               app.Dial(pjsip_extension, 60, 'mtT')
            else
               app.Wait(0.5)
            end
         end
      end,

   },
}
  1. pjsip.conf
[transport-tcp]
type = transport
protocol = tcp
bind = 0.0.0.0

[static-aor-template](!)
type = aor
max_contacts = 1
remove_existing = yes

[mobile-endpoint-template](!)
type = endpoint
context = default
disallow = all
allow = opus
rtp_symmetric = yes
force_rport = yes
rewrite_contact = yes
timers = no
direct_media = no
inband_progress = no
allow_subscribe = yes
dtmf_mode = rfc4733
ice_support = yes
transport = transport-tcp

[1001](static-aor-template)

[1001]
type = auth
username = 1001
password = Giethoh4

[1001](mobile-endpoint-template)
auth = 1001
outbound_auth = 1001
aors = 1001
callerid = "М. Иванов" <1001>
  1. manager.conf
[general]
enabled = yes
port = 5038
bindaddr = 127.0.0.1

[asterisk]
secret = 881d6256664648e0ebe1ed0e9b1340f2
read = all
write = all

About

API, используемое мобильными приложениями для iOS и Android / API for customers' mobile applications

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published