Metadata-Version: 2.4
Name: ast_route
Version: 0.9.1
Summary: Python phone router and support code
Author-email: Matthias Urlichs <smurf@noris.de>
License: This code is licensed under the LGPL-2.1 or later.
        
Project-URL: homepage, https://intra.office.noris.de/Wiki/Zoom/Phone
Project-URL: repository, https://gitlab.noris.net/voice/ast_route
Keywords: ast_route,sip
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Framework :: Trio
Classifier: Framework :: AnyIO
Classifier: License :: OSI Approved
Classifier: Development Status :: 4 - Beta
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
Requires-Dist: httpx
Requires-Dist: asyncopenapi3
Requires-Dist: asyncclick
Requires-Dist: requests
Requires-Dist: trio
Requires-Dist: cysystemd
Requires-Dist: hypercorn
Dynamic: license-file

======================
Asterisk Phone Routing
======================

This Python package performs external routing for Asterisk.

While Asterisk has an event-based interface (and a REST-based interface),
it lacks a simple straightforward way to forward phone calls from A to B.
Also, mangling phone numbers is rather awkward in Asterisk's AEL.

This package is useful if you want to write your phone number mangling
and rewriting in Python.

------------
Installation
------------

Install Asterisk and this module.

Create an appropriate configuration file.

Add this to ``extensions.ael``::

    // Your SIP calls arrive here. Add one of these for each SIP peer.
    context in_foo {
        includes { default; };
        _[+0-9]. => &aroute(foo,${EXTEN});
    }

    context default {
        h => Hangup();
        i => Hangup();
    }

    macro aroute(src,ext) {
        Verbose(0,CALL: ${src}:${CALLERID(NUM)}:${ext});
        Set(curl=http://localhost:50080/route/${src}:${CALLERID(NUM)}:${ext});
        Verbose(0,    = ${CURL(${curl})});
        Set(dispatch=${CURL(${curl})});
        &get(hangup); Set(hangup=${res});
        if("${hangup}" = "") {
            &get(chan); Set(chan=${res});
            if("${chan}" = "Local") {
                &get(dest); Set(dest=${res});
                Set(dest=Local/${STRREPLACE(dest,+,@in_${src}&Local/)}@in_${src})
                Dial(${chan}/${dest});
            } else {
                &get(a_nr); Set(a_nr=${res});
                &get(b_nr); Set(b_nr=${res});
                &get(p_nr); Set(p_nr=${res});
                &get(end); Set(end=${res});

                Set(CALLERID(num)=${a_nr});
                Dial(${chan}/${b_nr}@${end});
            }
        } else {
            // error
            PJSIPHangup(${hangup});
        }
        Hangup();
        return;
    }

    macro get(name) {
        // extract a field from the CURL result.
        //
        // dispatch="foo=bar:baz=quux"
        // get(foo) -> bar
        //
        Set(disp=${dispatch});
        while("${disp}"!="") {
            Set(assign=${CUT(disp,:,1)});
            Set(disp=${CUT(disp,:,2-99)});

            Set(var=${CUT(assign,=,1)});
            Set(res=${CUT(assign,=,2)});
            if("${var}"=="${name}") {
                return;
            }

        }
        Set(res=);
        return;
    }


Variables
---------

This dialplan accepts these parameters:

* a_nr
  number part of the caller id. This might need to be our SIP ID.
* p_nr
  The real a_nr (SIP: P-Preferred-Identity)
* b_nr
  Number to dial. This may need to be the destination SIP ID.
* chan
  Channel. You probably want PJSIP. To divert to a local channel use "Local".
* dest
  Destination endpoint. This is the dialplan context when diverting to a local channel.

or

* hangup
  PJSIP cause code.


For diverting a call to a local context, set chan=Local dest=context_name b_nr=extension.


-------------
Configuration
-------------
        
Bugs
----

`hypercorn`, which we use as an HTTP server, has an annoying bug
in releases up to and including 0.17.3: it ignores a `Connection: close`
header. This causes problems with clients like Icinga.

----------------------
Zoom Phone integration
----------------------

Get an API token from Zoom. It needs to be able to read your user records.

Create a ``secrets.yaml`` file::

    zoom:
      # API to them
      account: "xxxxxxxxxxxxxxxxxxxxxx"
      client: "xxxxxxxxxxxxxxxxxxxxxx"
      password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    
      # Sign replies to the callback from them
      # TODO
      #token: "xxxxxxxxxxxxxxxxxxxxxx"

Add something like this to your ``routing.yaml``::

    route:
    - match: "496990009(0|[1-6]{2,3})"
      dest: zoom
      use: known
    - match: "496990009(0|[1-6]{2,3})"
      dest: legacy_pbx

    legacy_pbx:
        fallback: zoom

    zoom:
        type: zoom
        fallback: legacy_pbx
    
        # Interval for updating extensions allocated on Zoom.
        # -1: read once (and when refreshing), 0: don't read at all
        update: 1200

        # Numbers to forward to Zoom, regardless of whether they are known
        known:
          - "496990009123"
        use: known  # only forward numbers that have been assigned
    
    route:
      - match: "(0|[1-6][0-9]{2,3})"
        result: "496990009$1"
        dest: legacy_pbx

        a_in: false
        b_in: true

        a_out: null
        b_out: true
        p_out: false
        chan: "PJSIP"  # default
        # end: "zoom"  # endpoint. Default: provider name

    
    zoom:
        # auth
        auth_url: https://zoom.us/oauth/token
    
        cred:
          # API
          account: !secret zoom.account
          client: !secret zoom.client
          secret: !secret zoom.password
    
          # callbacks
          #token: !secret zoom.token

The "fallback" causes a call that originates from this provider and
gets routed back to the same provider to be routed to the fallback provider
instead. 

