├── .coveragerc ├── .github ├── .github │ └── workflows │ │ └── tox.yml └── workflows │ └── tox.yml ├── .gitignore ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.rst ├── docs ├── Makefile ├── actions.rst ├── conf.py ├── conftest.py ├── fast_agi.rst ├── index.rst ├── manager.rst ├── message.rst └── testing.rst ├── examples ├── config.ini ├── event_listener.py ├── event_listener_modern.py ├── fast_agi_server.py ├── fast_agi_server_ivr.py ├── get_extension_status.py ├── md5_auth.py ├── originate.py ├── perpetual_ping.py └── queue_status.py ├── panoramisk ├── __init__.py ├── actions.py ├── ami_protocol.py ├── call_manager.py ├── command.py ├── exceptions.py ├── fast_agi.py ├── manager.py ├── message.py ├── testing.py └── utils.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── docker │ ├── asterisk-cli.sh │ ├── docker-compose.yml │ ├── docker-entrypoint.sh │ └── etc │ │ └── manager.conf ├── fixtures │ ├── agent_loggued_in.yaml │ ├── agent_not_in_pause.yaml │ ├── asyncagi_channel_does_not_exist.yaml │ ├── asyncagi_get_var.yaml │ ├── command_core_show_version.yaml │ ├── login_failed.yaml │ ├── login_md5.yaml │ ├── login_ok.yaml │ ├── logoff.yaml │ ├── originate_sync.yaml │ ├── originate_with_events.yaml │ ├── ping.yaml │ ├── pjsip_show_endpoint.yaml │ ├── queue_add.yaml │ ├── queue_status.yaml │ ├── rasterisk_command.yaml │ └── sip_notify.yaml ├── test_fast_agi.py ├── test_manager_with_asterisk.py ├── test_manager_with_fixtures.py ├── test_message.py ├── test_protocol.py └── test_utils.py ├── tools ├── README.rst ├── netcat-middleman.sh └── travis └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */panoramisk/command.py 4 | */panoramisk/call_manager.py 5 | include = 6 | */panoramisk/panoramisk/* 7 | 8 | [report] 9 | exclude_lines = 10 | pragma: no cover 11 | def __repr__ 12 | raise NotImplementedError 13 | if __name__ == .__main__.: 14 | def parse_args 15 | -------------------------------------------------------------------------------- /.github/.github/workflows/tox.yml: -------------------------------------------------------------------------------- 1 | name: tox 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tox: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python: [3.5, 3.6, 3.7, 3.8, 3.9] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python }} 19 | - name: Start Redis 20 | uses: supercharge/redis-github-action@1.1.0 21 | - name: Install Tox and any other packages 22 | run: pip install tox 23 | - name: Run Tox 24 | run: tox -e py 25 | - name: Run flake8 / docs 26 | run: tox -e flake8,docs 27 | if: matrix.python == 3.8 28 | -------------------------------------------------------------------------------- /.github/workflows/tox.yml: -------------------------------------------------------------------------------- 1 | name: tox 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tox: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Setup Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python }} 19 | - name: Pull images 20 | run: docker-compose -f tests/docker/docker-compose.yml pull 21 | - name: Install Tox and any other packages 22 | run: pip install tox 23 | - name: Run Tox 24 | run: tox -e py 25 | - name: Run flake8 / docs 26 | run: tox -e flake8,docs 27 | if: "matrix.python == '3.10' || matrix.python == '3.11' || matrix.python == '3.12'" 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bck 3 | bin 4 | build 5 | _build 6 | .bzr 7 | .bzrignore 8 | .cache 9 | .chutifab 10 | .coverage 11 | .coverage* 12 | coverage* 13 | develop-eggs 14 | dist 15 | downloads 16 | *.egg 17 | *.EGG 18 | *.egg-info 19 | *.EGG-INFO 20 | eggs 21 | fake-eggs 22 | .hg 23 | .hgignore 24 | .idea 25 | *.iml 26 | .installed.cfg 27 | *.jar 28 | *.mo 29 | .mr.developer.cfg 30 | nosetest* 31 | *.old 32 | *.orig 33 | parts 34 | *.pyc 35 | *.pyd 36 | *.pyo 37 | *.so 38 | src 39 | .svn 40 | *.swp 41 | .tox 42 | *.tmp* 43 | var 44 | *.wpr 45 | pyvenv/ 46 | .directory 47 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 1.5 (unreleased) 2 | ---------------- 3 | 4 | - Action is now both a Future and an async iterator 5 | 6 | 7 | 1.4 (2021-08-05) 8 | ---------------- 9 | 10 | - py38 support 11 | 12 | - no longer use `yield from` syntaxe. 13 | 14 | - Added custom options for connect method: run_forever, on_startup, 15 | on_shutdown, reconnect_timeout 16 | 17 | - Make ping interval and ping delay more configurable. Callbacks on_login, 18 | on_connect and on_disconnect added for use in metrics 19 | 20 | - Avoid double exception logging because log.exception() already logs the 21 | stacktrace 22 | 23 | - Multiple FastAGI support improvement 24 | 25 | 26 | 1.3 (2018-09-21) 27 | ---------------- 28 | 29 | - py37 support 30 | 31 | 32 | 1.2 (2018-05-24) 33 | ---------------- 34 | 35 | - Fix KeyError problem in call_manager 36 | 37 | - as_list is now default to none and is used first in Action.multi 38 | 39 | - improve AMI version detection 40 | 41 | 42 | 1.1 (2016-12-29) 43 | ---------------- 44 | 45 | - We no longer loosing calls on reconnection 46 | 47 | - End of support for python<3.4 48 | 49 | - Better test coverage 50 | 51 | 1.0 (2015-08-19) 52 | ---------------- 53 | 54 | - Add FastAGI server, implemented with high-level AsyncIO API (Streams) 55 | - Add events parameter for Manager, to disable AMI events reception (Useful for performance reasons) 56 | - Finish AsyncAGI commands support (WIP) 57 | 58 | 0.6 (2014-11-16) 59 | ---------------- 60 | 61 | - Avoid failure when factory is not already set 62 | 63 | 0.5 (2014-11-16) 64 | ---------------- 65 | 66 | - AMI command results tracking fixed 67 | - Return AMI command result with multiple events in a Future 68 | - Return AsyncAGI command result in a Future 69 | - Add several examples 70 | - Internal refactoring 71 | - Remove arawman support 72 | - Remove external dependencies 73 | - Add support for multiple responses from Actions (example: QueueStatus) 74 | - Improved performance with Events pattern matching 75 | - Add mocked test wrapper 76 | - Add coroutine support for Events dispatching 77 | - Invert event callback signature to create Manager methods to handle events 78 | - Support of AMI commands 79 | - Support of AsyncAGI commands (Not finished) 80 | 81 | 0.4 (2014-05-30) 82 | ---------------- 83 | 84 | - Compat with the latest trollius 85 | 86 | 87 | 0.3 (2014-01-10) 88 | ---------------- 89 | 90 | - Don't send commands twice 91 | 92 | 93 | 0.2 (2014-01-09) 94 | ---------------- 95 | 96 | - Initial release 97 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | Feel free to clone the project on `GitHub `_. 5 | 6 | Once you made a change, try to add a test for your feature/fix. At least assume 7 | that you have'nt broke anything by running tox:: 8 | 9 | $ tox 10 | ... 11 | py34: commands succeeded 12 | py35: commands succeeded 13 | flake8: commands succeeded 14 | docs: commands succeeded 15 | congratulations :) 16 | 17 | You can run tests for a specific version:: 18 | 19 | $ tox -e py35 20 | 21 | Running individual test:: 22 | 23 | $ tox -e py35 tests/test_manager.py::test_connection 24 | 25 | 26 | You can also build the docs with:: 27 | 28 | $ tox -e docs 29 | 30 | And check the result:: 31 | 32 | $ firefox .tox/docs/tmp/html/index.html 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Gael Pasgrimaud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft examples 2 | graft tools 3 | graft docs 4 | prune docs/_build 5 | prune .github/ 6 | graft panoramisk 7 | graft tests 8 | graft tests_python3 9 | include *.rst *.cfg *.ini Pipfile* .coveragerc LICENCE 10 | global-exclude *.pyc 11 | global-exclude __pycache__ 12 | global-exclude .installed.cfg 13 | include *.py 14 | include .coveragerc 15 | include LICENSE 16 | recursive-include examples *.py 17 | recursive-include tests_python3 *.py 18 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | 8 | [dev-packages] 9 | tox = "*" 10 | 11 | [requires] 12 | python_version = "3.7" 13 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "0b1c8e49c9289fc7ec8fee4ee9852693e20d7d02ec9805b0e1afc2800c89e025" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": { 20 | "appdirs": { 21 | "hashes": [ 22 | "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", 23 | "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" 24 | ], 25 | "version": "==1.4.4" 26 | }, 27 | "distlib": { 28 | "hashes": [ 29 | "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb", 30 | "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1" 31 | ], 32 | "version": "==0.3.1" 33 | }, 34 | "filelock": { 35 | "hashes": [ 36 | "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", 37 | "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" 38 | ], 39 | "version": "==3.0.12" 40 | }, 41 | "importlib-metadata": { 42 | "hashes": [ 43 | "sha256:19192b88d959336bfa6bdaaaef99aeafec179eca19c47c804e555703ee5f07ef", 44 | "sha256:2e881981c9748d7282b374b68e759c87745c25427b67ecf0cc67fb6637a1bff9" 45 | ], 46 | "markers": "python_version < '3.8'", 47 | "version": "==4.0.0" 48 | }, 49 | "packaging": { 50 | "hashes": [ 51 | "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", 52 | "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" 53 | ], 54 | "version": "==20.9" 55 | }, 56 | "pluggy": { 57 | "hashes": [ 58 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 59 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 60 | ], 61 | "version": "==0.13.1" 62 | }, 63 | "py": { 64 | "hashes": [ 65 | "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", 66 | "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" 67 | ], 68 | "index": "pypi", 69 | "version": "==1.10.0" 70 | }, 71 | "pyparsing": { 72 | "hashes": [ 73 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 74 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 75 | ], 76 | "version": "==2.4.7" 77 | }, 78 | "six": { 79 | "hashes": [ 80 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 81 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 82 | ], 83 | "version": "==1.15.0" 84 | }, 85 | "tox": { 86 | "hashes": [ 87 | "sha256:4df108a1fcc93a7ee4ac97e1a3a1fc3d41ddd22445d518976604e2ef05025280", 88 | "sha256:9f0cbcc36e08c2c4ae90d02d3d1f9a62231f974bcbc1df85e8045946d8261059" 89 | ], 90 | "index": "pypi", 91 | "version": "==3.1.2" 92 | }, 93 | "typing-extensions": { 94 | "hashes": [ 95 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 96 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 97 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 98 | ], 99 | "markers": "python_version < '3.8'", 100 | "version": "==3.7.4.3" 101 | }, 102 | "virtualenv": { 103 | "hashes": [ 104 | "sha256:09c61377ef072f43568207dc8e46ddeac6bcdcaf288d49011bda0e7f4d38c4a2", 105 | "sha256:a935126db63128861987a7d5d30e23e8ec045a73840eeccb467c148514e29535" 106 | ], 107 | "version": "==20.4.4" 108 | }, 109 | "zipp": { 110 | "hashes": [ 111 | "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", 112 | "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" 113 | ], 114 | "version": "==3.4.1" 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | Panoramisk. The Asterisk's druid 3 | ================================ 4 | 5 | .. image:: https://travis-ci.org/gawel/panoramisk.png?branch=master&style=flat-square 6 | :target: https://travis-ci.org/gawel/panoramisk 7 | 8 | .. image:: https://img.shields.io/coveralls/gawel/panoramisk/master.svg 9 | :target: https://coveralls.io/r/gawel/panoramisk?branch=master 10 | 11 | .. image:: https://img.shields.io/pypi/v/panoramisk.svg?style=flat-square 12 | :target: https://pypi.python.org/pypi/panoramisk 13 | 14 | .. image:: https://img.shields.io/pypi/dw/panoramisk.svg?style=flat-square 15 | :target: https://pypi.python.org/pypi/panoramisk 16 | 17 | .. image:: https://img.shields.io/github/issues/gawel/panoramisk.svg?style=flat-square 18 | :target: https://github.com/gawel/panoramisk/issues 19 | 20 | .. image:: https://img.shields.io/github/license/gawel/panoramisk.svg?style=flat-square 21 | :target: https://github.com/gawel/panoramisk/blob/master/LICENSE 22 | 23 | 24 | `Panoramisk` is a library based on python's `AsyncIO 25 | `_ to play with `Asterisk 26 | `_'s `manager 27 | `_. 28 | 29 | It uses the TCP manager server to listen to events and send actions. 30 | 31 | For basic usage, you have some examples in `examples/ 32 | `_ folder. 33 | 34 | You can find some help on IRC: irc://irc.freenode.net/panoramisk (`www 35 | `_) 36 | 37 | 38 | Source code is available at https://github.com/gawel/panoramisk/ 39 | 40 | 41 | Check the `full documentation `_ 42 | 43 | 44 | I've spent hours writing this software, with love. 45 | Please consider tiping if you like it: 46 | 47 | BTC: 1PruQAwByDndFZ7vTeJhyWefAghaZx9RZg 48 | 49 | ETH: 0xb6418036d8E06c60C4D91c17d72Df6e1e5b15CE6 50 | 51 | LTC: LY6CdZcDbxnBX9GFBJ45TqVj8NykBBqsmT 52 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/panoramisk.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/panoramisk.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/panoramisk" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/panoramisk" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/actions.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | :mod:`panoramisk.actions` 3 | ========================= 4 | 5 | .. automodule:: panoramisk.actions 6 | 7 | .. autoclass:: Action 8 | :members: 9 | 10 | .. autoclass:: Command 11 | :members: 12 | 13 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # panoramisk documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Jan 9 12:06:32 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'panoramisk' 44 | copyright = u'2014, Gael Pasgrimaud' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'panoramiskdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'panoramisk.tex', u'panoramisk Documentation', 187 | u'Gael Pasgrimaud', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'panoramisk', u'panoramisk Documentation', 217 | [u'Gael Pasgrimaud'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'panoramisk', u'panoramisk Documentation', 231 | u'Gael Pasgrimaud', 'panoramisk', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | 244 | html_theme = 'nature' 245 | import pkg_resources 246 | version = pkg_resources.get_distribution("panoramisk").version 247 | release = version 248 | -------------------------------------------------------------------------------- /docs/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def inject_file(): 8 | return __file__ 9 | 10 | -------------------------------------------------------------------------------- /docs/fast_agi.rst: -------------------------------------------------------------------------------- 1 | :mod:`panoramisk.fast_agi` - Fast AGI 2 | ===================================== 3 | 4 | An API to create Fast AGI applications 5 | 6 | API 7 | --- 8 | 9 | .. automodule:: panoramisk.fast_agi 10 | 11 | .. autoclass:: Application 12 | :members: 13 | 14 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. panoramisk documentation master file, created by 2 | sphinx-quickstart on Thu Jan 9 12:06:32 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. include:: ../README.rst 7 | 8 | ============= 9 | Installation 10 | ============= 11 | 12 | Install, upgrade and uninstall panoramisk with these commands: 13 | 14 | .. code-block:: sh 15 | 16 | $ pip install panoramisk 17 | $ pip install --upgrade panoramisk 18 | $ pip uninstall panoramisk 19 | 20 | 21 | ======================== 22 | Detailed Documentation 23 | ======================== 24 | 25 | .. toctree:: 26 | :maxdepth: 1 27 | 28 | manager.rst 29 | fast_agi.rst 30 | testing.rst 31 | 32 | 33 | =================================== 34 | Who use Panoramisk on production ? 35 | =================================== 36 | 37 | For now, mainly `Eyepea 38 | `_ and `ALLOcloud 39 | `_. 40 | 41 | You shouldn't know theses companies, however, Eyepea is a provider of several famous European companies and governments organizations. 42 | You can check their references on their website: 43 | 44 | * http://www.eyepea.eu/customers 45 | * http://www.eyepea.eu/company/news 46 | 47 | Moreover, ALLOcloud is a cloud solution for SMEs, it handles several millions of calls by month. 48 | 49 | If you also use Panoramisk on production, don't hesitate to open a pull request to add your company's name with some details. 50 | 51 | ======= 52 | CHANGES 53 | ======= 54 | 55 | .. include:: ../CHANGES.rst 56 | 57 | ================== 58 | Indices and tables 59 | ================== 60 | 61 | * :ref:`genindex` 62 | * :ref:`modindex` 63 | * :ref:`search` 64 | 65 | -------------------------------------------------------------------------------- /docs/manager.rst: -------------------------------------------------------------------------------- 1 | :mod:`panoramisk` - AMI 2 | ======================== 3 | 4 | An API to communicate with Asterisk's AMI 5 | 6 | Configure Asterisk 7 | -------------------------- 8 | 9 | In ``/etc/asterisk/manager.conf``, add: 10 | 11 | .. code-block:: ini 12 | 13 | [username] 14 | secret=password 15 | deny=0.0.0.0/0.0.0.0 16 | permit=127.0.0.1/255.255.255.255 17 | read = all 18 | write = all 19 | 20 | Launch: 21 | 22 | .. code-block:: sh 23 | 24 | $ rasterisk -x 'manager reload' 25 | 26 | 27 | API 28 | --- 29 | 30 | .. automodule:: panoramisk 31 | 32 | .. autoclass:: Manager 33 | :members: 34 | -------------------------------------------------------------------------------- /docs/message.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | :mod:`panoramisk.message` 3 | ========================= 4 | 5 | .. automodule:: panoramisk.message 6 | 7 | .. autoclass:: Message 8 | :members: 9 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | ========================================== 2 | :mod:`panoramisk.testing` - Writing tests 3 | ========================================== 4 | 5 | .. 6 | >>> import os 7 | >>> stream = os.path.join('tests', 'fixtures', 'ping.yaml') 8 | 9 | .. code-block:: python 10 | 11 | >>> from panoramisk import testing 12 | >>> manager = testing.Manager(stream=stream) # stream is a filename contaning an Asterisk trace 13 | >>> future = manager.send_action({'Action': 'Ping'}) 14 | >>> resp = future.result() 15 | >>> assert 'ping' in resp 16 | >>> assert resp.ping == 'Pong' 17 | 18 | API 19 | --- 20 | 21 | .. automodule:: panoramisk.testing 22 | 23 | .. autoclass:: Manager 24 | :members: 25 | -------------------------------------------------------------------------------- /examples/config.ini: -------------------------------------------------------------------------------- 1 | [asterisk] 2 | host=127.0.0.1 3 | username=username 4 | secret=mysecret 5 | -------------------------------------------------------------------------------- /examples/event_listener.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from panoramisk import Manager 3 | 4 | manager = Manager(loop=asyncio.get_event_loop(), 5 | host='127.0.0.1', 6 | username='user', 7 | secret='mysecret') 8 | 9 | 10 | @manager.register_event('*') 11 | def callback(manager, message): 12 | if "FullyBooted" not in message.event: 13 | """This will print every event, but the FullyBooted events as these 14 | will continuously spam your screen""" 15 | print(message) 16 | 17 | 18 | """ 19 | # This will print NewChannel Events 20 | @manager.register_event('NewChannel') 21 | def callback(manager, message): 22 | print(message) 23 | 24 | 25 | # This will print Hangup Events 26 | @manager.register_event('Hangup') 27 | def callback(manager, message): 28 | print(message) 29 | """ 30 | 31 | 32 | def main(): 33 | manager.connect() 34 | try: 35 | manager.loop.run_forever() 36 | except KeyboardInterrupt: 37 | manager.loop.close() 38 | 39 | 40 | if __name__ == '__main__': 41 | main() 42 | -------------------------------------------------------------------------------- /examples/event_listener_modern.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | 5 | from panoramisk import Manager, Message 6 | 7 | 8 | manager = Manager( 9 | host=os.getenv('AMI_HOST', '127.0.0.1'), 10 | port=os.getenv('AMI_PORT', 5038), 11 | username=os.getenv('AMI_USERNAME', 'username'), 12 | secret=os.getenv('AMI_SECRET', 'mysecret'), 13 | ping_delay=10, # Delay after start 14 | ping_interval=10, # Periodically ping AMI (dead or alive) 15 | reconnect_timeout=2, # Timeout reconnect if connection lost 16 | ) 17 | 18 | 19 | def on_connect(mngr: Manager): 20 | logging.info( 21 | 'Connected to %s:%s AMI socket successfully' % 22 | (mngr.config['host'], mngr.config['port']) 23 | ) 24 | 25 | 26 | def on_login(mngr: Manager): 27 | logging.info( 28 | 'Connected user:%s to AMI %s:%s successfully' % 29 | (mngr.config['username'], mngr.config['host'], mngr.config['port']) 30 | ) 31 | 32 | 33 | def on_disconnect(mngr: Manager, exc: Exception): 34 | logging.info( 35 | 'Disconnect user:%s from AMI %s:%s' % 36 | (mngr.config['username'], mngr.config['host'], mngr.config['port']) 37 | ) 38 | logging.debug(str(exc)) 39 | 40 | 41 | async def on_startup(mngr: Manager): 42 | await asyncio.sleep(0.1) 43 | logging.info('Something action...') 44 | 45 | 46 | async def on_shutdown(mngr: Manager): 47 | await asyncio.sleep(0.1) 48 | logging.info( 49 | 'Shutdown AMI connection on %s:%s' % (mngr.config['host'], mngr.config['port']) 50 | ) 51 | 52 | 53 | @manager.register_event('*') # Register all events 54 | async def ami_callback(mngr: Manager, msg: Message): 55 | if msg.Event == 'FullyBooted': 56 | print(msg) 57 | 58 | 59 | if __name__ == '__main__': 60 | logging.basicConfig(level=logging.INFO) 61 | manager.on_connect = on_connect 62 | manager.on_login = on_login 63 | manager.on_disconnect = on_disconnect 64 | manager.connect(run_forever=True, on_startup=on_startup, on_shutdown=on_shutdown) 65 | -------------------------------------------------------------------------------- /examples/fast_agi_server.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | import asyncio 3 | from panoramisk import fast_agi 4 | 5 | 6 | async def call_waiting(request): 7 | pprint(['AGI variables:', request.headers]) 8 | pprint((await request.send_command('ANSWER'))) 9 | pprint((await request.send_command('EXEC StartMusicOnHold'))) 10 | pprint((await request.send_command('EXEC Wait 30'))) 11 | 12 | 13 | async def main(): 14 | fa_app = fast_agi.Application() 15 | fa_app.add_route('call_waiting', call_waiting) 16 | server = await asyncio.start_server(fa_app.handler, '0.0.0.0', 4574) 17 | 18 | # Serve requests until CTRL+c is pressed 19 | print('Serving on {}'.format(server.sockets[0].getsockname())) 20 | try: 21 | await server.serve_forever() 22 | except KeyboardInterrupt: 23 | pass 24 | finally: 25 | # Close the server 26 | server.close() 27 | 28 | 29 | if __name__ == '__main__': 30 | asyncio.run(main()) 31 | -------------------------------------------------------------------------------- /examples/fast_agi_server_ivr.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | import asyncio 3 | from panoramisk import fast_agi 4 | 5 | 6 | async def call_waiting(request): 7 | pprint(['AGI variables:', request.headers]) 8 | 9 | pprint((await request.send_command('ANSWER'))) 10 | pprint((await request.send_command('SAY DIGITS 1 \"\"'))) 11 | 12 | # To Raise a 510 error - 510 Invalid or unknown command 13 | pprint((await request.send_command('INVALID-COMMAND'))) 14 | 15 | # To Raise a 520 error - 520-Invalid command syntax. Proper usage follows: 16 | pprint((await request.send_command('SAY PHONETIC Hello world .'))) 17 | 18 | pprint((await request.send_command('SAY NUMBER 100 \"\"'))) 19 | pprint((await request.send_command('GET DATA hello-world 5000 2'))) 20 | 21 | pprint((await request.send_command('EXEC StartMusicOnHold'))) 22 | pprint((await request.send_command('EXEC Wait 30'))) 23 | 24 | 25 | async def main(): 26 | fa_app = fast_agi.Application() 27 | fa_app.add_route('call_waiting', call_waiting) 28 | server = asyncio.start_server(fa_app.handler, '0.0.0.0', 4574) 29 | 30 | # Serve requests until CTRL+c is pressed 31 | print('Serving on {}'.format(server.sockets[0].getsockname())) 32 | try: 33 | await server.serve_forever() 34 | except KeyboardInterrupt: 35 | pass 36 | finally: 37 | # Close the server 38 | server.close() 39 | 40 | 41 | if __name__ == '__main__': 42 | asyncio.run(main()) 43 | -------------------------------------------------------------------------------- /examples/get_extension_status.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | import asyncio 3 | from panoramisk import Manager 4 | 5 | 6 | async def extension_status(): 7 | manager = Manager(loop=asyncio.get_event_loop(), 8 | host='127.0.0.1', port=5038, 9 | username='username', secret='mysecret') 10 | await manager.connect() 11 | action = { 12 | 'Action': 'ExtensionState', 13 | 'Exten': '2001', 14 | 'Context': 'default', 15 | } 16 | extension = await manager.send_action(action) 17 | pprint(extension) 18 | manager.close() 19 | 20 | 21 | def main(): 22 | loop = asyncio.get_event_loop() 23 | loop.run_until_complete(extension_status()) 24 | loop.close() 25 | 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /examples/md5_auth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from panoramisk import Manager, Message 5 | 6 | 7 | manager = Manager( 8 | host=os.getenv('AMI_HOST', '127.0.0.1'), 9 | port=os.getenv('AMI_PORT', 5038), 10 | username=os.getenv('AMI_USERNAME', 'username'), 11 | secret=os.getenv('AMI_SECRET', 'mysecret'), 12 | auth_type='md5', # MD5 auth, no case sensitive 13 | ping_delay=10, # Delay after start 14 | ping_interval=10, # Periodically ping AMI (dead or alive) 15 | reconnect_timeout=2, # Timeout reconnect if connection lost 16 | ) 17 | 18 | 19 | @manager.register_event('*') # Register all events 20 | async def ami_callback(mngr: Manager, msg: Message): 21 | if msg.Event == 'FullyBooted': 22 | print(msg) 23 | 24 | 25 | if __name__ == '__main__': 26 | logging.basicConfig(level=logging.INFO) 27 | manager.connect(run_forever=True) 28 | -------------------------------------------------------------------------------- /examples/originate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example script to originate a call through Asterisk Manager Interface. 3 | 4 | Usage: python originate.py config.ini 5 | 6 | """ 7 | import sys 8 | import asyncio 9 | from panoramisk.call_manager import CallManager 10 | 11 | 12 | async def originate(): 13 | callmanager = CallManager.from_config(sys.argv[1]) 14 | await callmanager.connect() 15 | call = await callmanager.send_originate({ 16 | 'Action': 'Originate', 17 | 'Channel': 'Local/gpasgrimaud@bearstech', 18 | 'WaitTime': 20, 19 | 'CallerID': 'gawel', 20 | 'Exten': '1000', 21 | 'Context': 'ael-demo', 22 | 'Priority': 1}) 23 | print(call) 24 | while not call.queue.empty(): 25 | event = call.queue.get_nowait() 26 | print(event) 27 | while True: 28 | event = await call.queue.get() 29 | print(event) 30 | if event.event.lower() == 'hangup' and event.cause in ('0', '17'): 31 | break 32 | callmanager.clean_originate(call) 33 | callmanager.close() 34 | 35 | 36 | def main(): 37 | loop = asyncio.get_event_loop() 38 | loop.run_until_complete(originate()) 39 | loop.close() 40 | 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /examples/perpetual_ping.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | import asyncio 3 | from panoramisk import Manager 4 | 5 | 6 | async def ping(): 7 | manager = Manager(host='127.0.0.1', port=5038, 8 | username='username', secret='mysecret', 9 | forgetable_actions=('login',)) 10 | await manager.connect() 11 | while True: 12 | p = await manager.send_action({'Action': 'ping'}) 13 | # p = await manager.send_action({'Action': 'SIPpeers'}) 14 | pprint(p) 15 | await asyncio.sleep(1) 16 | manager.close() 17 | 18 | 19 | def main(): 20 | asyncio.run(ping()) 21 | 22 | 23 | if __name__ == '__main__': 24 | main() 25 | -------------------------------------------------------------------------------- /examples/queue_status.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from panoramisk import Manager 3 | from pprint import pprint 4 | 5 | 6 | async def queue_status(): 7 | manager = Manager(host='127.0.0.1', port=5038, 8 | username='username', secret='mysecret') 9 | await manager.connect() 10 | action = {'Action': 'QueueStatus', 'Queue': 'queue_name'} 11 | async for message in manager.send_action(action): 12 | pprint(message) 13 | manager.close() 14 | 15 | 16 | def main(): 17 | asyncio.run(queue_status()) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /panoramisk/__init__.py: -------------------------------------------------------------------------------- 1 | from .manager import Manager # NOQA 2 | from .message import Message # NOQA 3 | from .call_manager import CallManager # NOQA 4 | from . import fast_agi # NOQA 5 | -------------------------------------------------------------------------------- /panoramisk/actions.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from . import utils 4 | 5 | 6 | class Action(utils.CaseInsensitiveDict, asyncio.Future): 7 | """Dict like object to handle actions. 8 | Generate action IDs for you: 9 | 10 | .. 11 | >>> utils.IdGenerator.reset('myuuid') 12 | 13 | .. code-block:: python 14 | 15 | >>> action = Action({'Action': 'Status'}) 16 | >>> print(action) # doctest: +NORMALIZE_WHITESPACE 17 | Action: Status 18 | ActionID: action/myuuid/1/1 19 | 20 | >>> action = Action({'Action': 'SIPnotify', 21 | ... 'Variable': ['1', '2']}) 22 | >>> print(action) # doctest: +NORMALIZE_WHITESPACE 23 | Action: SIPnotify 24 | ActionID: action/myuuid/1/2 25 | Variable: 1 26 | Variable: 2 27 | """ 28 | 29 | action_id_generator = utils.IdGenerator('action') 30 | 31 | def __init__(self, *args, **kwargs): 32 | self.as_list = kwargs.pop('as_list', None) 33 | super(Action, self).__init__(*args, **kwargs) 34 | asyncio.Future.__init__(self) 35 | if 'actionid' not in self: 36 | self['ActionID'] = self.action_id_generator() 37 | self.responses = [] 38 | self.responses_index = 0 39 | 40 | def __aiter__(self): 41 | return self 42 | 43 | async def __anext__(self): 44 | while True: 45 | if self.responses_index < len(self.responses): 46 | res = self.responses[self.responses_index] 47 | self.responses_index += 1 48 | return res 49 | elif self.done(): 50 | raise StopAsyncIteration 51 | else: 52 | await asyncio.sleep(.1) 53 | 54 | @property 55 | def id(self): 56 | return self.actionid 57 | 58 | action_id = id 59 | 60 | def __str__(self): 61 | action = [] 62 | for k, v in sorted(self.items()): 63 | if isinstance(v, (list, tuple)): 64 | action.extend(['%s: %s' % (k, i) for i in v]) 65 | else: 66 | action.append('%s: %s' % (k, v)) 67 | action.append(utils.EOL) 68 | return utils.EOL.join(action) 69 | 70 | @property 71 | def multi(self): 72 | resp = self.responses[0] 73 | msg = resp.message.lower() 74 | if self.as_list is not None: 75 | return bool(self.as_list) 76 | elif resp.subevent == 'Start': 77 | return True 78 | elif 'EventList' in resp and resp['EventList'] == 'start': 79 | return True 80 | elif 'will follow' in msg: 81 | return True 82 | elif msg == 'added interface to queue': 83 | return False 84 | elif msg.startswith('added') and msg.endswith('to queue'): 85 | return True 86 | elif msg.endswith('successfully queued') and self['async'] != 'false': 87 | return True 88 | return False 89 | 90 | @property 91 | def completed(self): 92 | resp = self.responses[-1] 93 | if resp.event.endswith('Complete'): 94 | return True 95 | elif resp.subevent in ('End', 'Exec'): 96 | return True 97 | elif resp.event in ('AsyncAGIExec',): 98 | return True 99 | elif resp.response in ('Success', 'Error', 'Fail', 'Failure'): 100 | return True 101 | elif not self.multi: 102 | return True 103 | return False 104 | 105 | def add_message(self, message): 106 | self.responses.append(message) 107 | multi = self.multi 108 | if self.completed and not self.done(): 109 | if multi and len(self.responses) > 1: 110 | self.set_result(self.responses) 111 | elif not multi: 112 | self.set_result(self.responses[0]) 113 | else: 114 | return False 115 | return True 116 | 117 | 118 | class Command(Action): 119 | """Dict like object to handle Commands. 120 | Generate action/command IDs for you: 121 | 122 | .. 123 | >>> utils.IdGenerator.reset('myuuid') 124 | 125 | .. code-block:: python 126 | 127 | >>> command = Command({'Command' : 'Do something'}) 128 | >>> print(command) # doctest: +NORMALIZE_WHITESPACE 129 | Action: Command 130 | ActionID: action/myuuid/1/1 131 | Command: Do something 132 | CommandID: command/myuuid/1/1 133 | """ 134 | 135 | command_id_generator = utils.IdGenerator('command') 136 | 137 | def __init__(self, *args, **kwargs): 138 | super(Command, self).__init__(*args, **kwargs) 139 | if 'action' not in self: 140 | self['Action'] = 'Command' 141 | if 'commandid' not in self: 142 | self['CommandID'] = self.command_id_generator() 143 | 144 | @property 145 | def id(self): 146 | return self.commandid 147 | 148 | @property 149 | def action_id(self): 150 | return self.actionid or None 151 | -------------------------------------------------------------------------------- /panoramisk/ami_protocol.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | from collections import deque 4 | 5 | from .message import Message 6 | from . import actions 7 | from . import utils 8 | 9 | 10 | class AMIProtocol(asyncio.Protocol): 11 | 12 | def connection_made(self, transport): 13 | self.transport = transport 14 | self.closed = False 15 | self.queue = deque() 16 | self.responses = {} 17 | self.factory = None 18 | self.version = None 19 | self.log = logging.getLogger(__name__) 20 | 21 | def send(self, data, as_list=False): 22 | encoding = getattr(self, 'encoding', 'ascii') 23 | if not isinstance(data, actions.Action): 24 | if 'Command' in data: 25 | klass = actions.Command 26 | else: 27 | klass = actions.Action 28 | data = klass(data, as_list=as_list) 29 | data.as_list = as_list 30 | self.responses[data.id] = data 31 | if data.action_id: 32 | self.responses[data.action_id] = data 33 | try: 34 | self.transport.write(str(data).encode(encoding)) 35 | except Exception: # pragma: no cover 36 | self.log.exception('Fail to send %r' % data) 37 | return data 38 | 39 | def data_received(self, data): 40 | encoding = getattr(self, 'encoding', 'ascii') 41 | data = data.decode(encoding, 'ignore') 42 | if getattr(self.factory, 'save_stream', None): # pragma: no cover 43 | stream = self.factory.save_stream 44 | if hasattr(stream, 'write'): 45 | stream.write(data.encode(encoding)) 46 | else: 47 | with open(stream, 'a+') as fd: 48 | fd.write(data.encode(encoding)) 49 | # Very verbose, uncomment only if necessary 50 | # self.log.debug('data received: "%s"', data) 51 | 52 | if self.version is None: 53 | if data.startswith('Asterisk Call Manager/'): 54 | version, __, __ = data.partition(utils.EOL) 55 | __, __, version = version.partition('/') 56 | self.version = version.strip() 57 | self.log.info("protocol version: '%s'", self.version) 58 | 59 | if self.queue: 60 | data = self.queue.popleft() + data 61 | lines = data.split(utils.EOL+utils.EOL) 62 | self.queue.append(lines.pop(-1)) 63 | for line in lines: 64 | # Because sometimes me receive only one EOL from Asterisk 65 | line = line.strip() 66 | # Very verbose, uncomment only if necessary 67 | # self.log.debug('message received: "%s"', line) 68 | message = Message.from_line(line) 69 | self.log.debug('message interpreted: %r', message) 70 | if message is None: 71 | continue 72 | self.handle_message(message) 73 | 74 | def handle_message(self, message): 75 | response = self.responses.get(message.id) 76 | if response is None and message.action_id: 77 | response = self.responses.get(message.action_id) 78 | if response is not None: 79 | if response.add_message(message): 80 | # completed; dequeue 81 | self.responses.pop(response.id) 82 | if response.action_id: 83 | self.responses.pop(response.action_id, None) 84 | elif 'event' in message: 85 | if message['event'].lower() == 'shutdown': 86 | self.connection_lost(message) 87 | self.factory.dispatch(message) 88 | 89 | def connection_lost(self, exc): 90 | if not self.closed: 91 | self.close() 92 | self.factory.connection_lost(exc) 93 | 94 | def close(self): 95 | if self.factory and self.responses: 96 | uuids = set() 97 | forgetable_actions = self.factory.forgetable_actions 98 | awaiting_actions = self.factory.awaiting_actions 99 | for k in list(self.responses.keys()): 100 | action = self.responses.pop(k) 101 | uuids.add(action.id) 102 | if action['action'].lower() in forgetable_actions: 103 | continue 104 | elif action.done(): # pragma: no cover 105 | continue 106 | elif action.responses: 107 | # If at least one response was receive from asterisk we don't queue it again 108 | continue 109 | else: 110 | self.log.info('Adding action "%s" to awaiting list: %s', action['action'].lower(), str(action)) 111 | awaiting_actions.append(action) 112 | if not self.closed: 113 | try: 114 | self.transport.close() 115 | finally: 116 | self.closed = True 117 | -------------------------------------------------------------------------------- /panoramisk/call_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from . import manager 3 | from . import actions 4 | from datetime import datetime 5 | from functools import partial 6 | 7 | 8 | class Call: 9 | 10 | def __init__(self, uniqueid): 11 | self.uniqueid = uniqueid 12 | self.action_id = None 13 | self.queue = asyncio.Queue() 14 | self.created_at = datetime.now() 15 | 16 | def append(self, *events): 17 | for e in events: 18 | self.queue.put_nowait(e) 19 | 20 | 21 | class CallManager(manager.Manager): 22 | 23 | def __init__(self, **config): 24 | super(CallManager, self).__init__(**config) 25 | self.CallClass = config.get('CallClass', Call) 26 | self.calls_queues = {} 27 | self.calls = {} 28 | self.register_event('*', self.handle_calls) 29 | 30 | def set_result(self, future, result): 31 | res = result.result() 32 | if isinstance(res, (list, tuple)): 33 | event = res[-1] 34 | else: 35 | event = res 36 | uniqueid = event.uniqueid.split('.', 1)[0] 37 | if uniqueid in self.calls_queues: 38 | call = self.calls_queues[uniqueid] 39 | else: 40 | call = Call(uniqueid) 41 | call.action_id = event.action_id 42 | if not future.done(): 43 | future.set_result(call) 44 | 45 | def send_originate(self, action): 46 | action['Async'] = 'true' 47 | action = actions.Action(action) 48 | future = self.loop.create_future() 49 | self.send_action(action).add_done_callback( 50 | partial(self.set_result, future)) 51 | return future 52 | 53 | def clean_originate(self, call): 54 | self.calls_queues.pop(call.uniqueid, None) 55 | 56 | def handle_calls(self, manager, event): 57 | uniqueid = event.uniqueid or event.uniqueid1 58 | if uniqueid: 59 | uniqueid = uniqueid.split('.', 1)[0] 60 | call = self.calls_queues.setdefault(uniqueid, Call(uniqueid)) 61 | call.append(event) 62 | -------------------------------------------------------------------------------- /panoramisk/command.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | import argparse 4 | import logging 5 | from . import utils 6 | from .call_manager import CallManager 7 | from .call_manager import Call 8 | 9 | try: 10 | import yaml 11 | except ImportError: 12 | yaml = None 13 | 14 | 15 | def main(argv=None): 16 | parser = argparse.ArgumentParser() 17 | if yaml is None: 18 | parser.error('You must install pyaml') 19 | parser.add_argument('-c', '--config', 20 | type=argparse.FileType('r'), 21 | required=True, 22 | help='Config ini file') 23 | parser.add_argument('-i', '--input', 24 | type=argparse.FileType('r'), 25 | help='Input yaml file') 26 | parser.add_argument('-o', '--output', 27 | type=argparse.FileType('w'), 28 | default='-', 29 | help='Stream output file (Default to STDOUT)') 30 | args = parser.parse_args(argv or sys.argv[1:]) 31 | config = utils.config(args.config) 32 | config['save_stream'] = args.output 33 | call_manager = CallManager(**config) 34 | task = call_manager.connect() 35 | 36 | logging.basicConfig(stream=sys.stderr, level=logging.INFO) 37 | logging.getLogger('asyncio').setLevel(logging.ERROR) 38 | log = logging.getLogger('panoramisk') 39 | 40 | def done(future): 41 | result = future.result() 42 | log.info('Got result: %r\n', result) 43 | if isinstance(result, Call): 44 | while not result.queue.empty(): 45 | print(result.queue.get_nowait()) 46 | 47 | def show(f=None): 48 | if f: 49 | print(f.result()) 50 | f = asyncio.create_task(result.queue.get()) 51 | f.add_done_callback(show) 52 | show() 53 | 54 | def send_action(f): 55 | if args.input: 56 | action = yaml.load(args.input) 57 | if action.get('Action').lower() == 'originate': 58 | future = call_manager.send_originate(action) 59 | elif 'commandid' in [k.lower() for k in action.keys()]: 60 | future = call_manager.send_command(action) 61 | else: 62 | future = call_manager.send_action(action) 63 | log.info('Action %r sent', action) 64 | future.add_done_callback(done) 65 | 66 | def connected(f): 67 | if call_manager.authenticated_future is not None: 68 | call_manager.authenticated_future.add_done_callback(send_action) 69 | else: 70 | send_action(f) 71 | 72 | task.add_done_callback(connected) 73 | 74 | try: 75 | call_manager.loop.run_forever() 76 | except KeyboardInterrupt: 77 | args.output.close() 78 | -------------------------------------------------------------------------------- /panoramisk/exceptions.py: -------------------------------------------------------------------------------- 1 | class AGIException(Exception): 2 | """The base exception for all AGI-related exceptions. 3 | """ 4 | 5 | def __init__(self, message, items): 6 | Exception.__init__(self, message) 7 | self.items = items # A dictionary containing data received from Asterisk, if any 8 | 9 | 10 | class AGIResultHangup(AGIException): 11 | """Indicates that Asterisk received a hangup event. 12 | """ 13 | 14 | 15 | class AGIError(AGIException): 16 | """The base exception for all AGI errors resulting from improper usage or Asterisk bug. 17 | """ 18 | 19 | 20 | class AGINoResultError(AGIError): 21 | """Indicates that Asterisk did not return a 'result' parameter in a 200 response. 22 | """ 23 | 24 | 25 | class AGIUnknownError(AGIError): 26 | """Indicates that an unknown response is received from Asterisk. 27 | """ 28 | 29 | 30 | class AGIAppError(AGIError): 31 | """Indicates that an Asterisk application failed to execute. 32 | """ 33 | 34 | 35 | class AGIDeadChannelError(AGIError): 36 | """Indicates that a command was issued on a channel that can no longer process it. 37 | """ 38 | 39 | 40 | class AGIInvalidCommand(AGIError): 41 | """Indicates that a request made to Asterisk was not understood. 42 | """ 43 | 44 | 45 | class AGIUsageError(AGIError): 46 | """Indicates that a request made to Asterisk was sent with invalid syntax. 47 | """ 48 | -------------------------------------------------------------------------------- /panoramisk/fast_agi.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | from collections import OrderedDict 4 | from .exceptions import AGIException, AGIUsageError 5 | from .utils import parse_agi_result 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | class Request: 11 | def __init__(self, app, headers, reader, writer, encoding='utf-8'): 12 | self.app = app 13 | self.headers = headers 14 | self.reader = reader 15 | self.writer = writer 16 | self.encoding = encoding 17 | 18 | async def send_command(self, command): 19 | """Send a command for FastAGI request: 20 | 21 | :param command: Command to launch on FastAGI request. Ex: 'EXEC StartMusicOnHolds' 22 | :type command: String 23 | 24 | :Example: 25 | 26 | :: 27 | 28 | async def call_waiting(request): 29 | print(['AGI variables:', request.headers]) 30 | await request.send_command('ANSWER') 31 | await request.send_command('EXEC StartMusicOnHold') 32 | await request.send_command('EXEC Wait 10') 33 | 34 | """ 35 | command += '\n' 36 | self.writer.write(command.encode(self.encoding)) 37 | await self.writer.drain() 38 | 39 | try: 40 | agi_result = await self._read_result() 41 | # If Asterisk returns `100 Trying...`, wait for next the response. 42 | while agi_result.get('status_code') == 100: 43 | agi_result = await self._read_result() 44 | except AGIUsageError as err: 45 | message = err.args[0] 46 | # When we get AGIUsageError the following line contains some indication 47 | buff_usage_error = await self.reader.readline() 48 | message += buff_usage_error.decode(self.encoding) 49 | if self.app.raise_on_error: 50 | raise AGIUsageError(message, err.items) 51 | agi_result = err.items 52 | agi_result['error'] = err.__class__.__name__ 53 | agi_result['msg'] = message 54 | except AGIException as err: 55 | if self.app.raise_on_error: 56 | raise 57 | agi_result = err.items 58 | agi_result['error'] = err.__class__.__name__ 59 | agi_result['msg'] = err.args[0] 60 | 61 | return agi_result 62 | 63 | async def _read_result(self): 64 | """Read a response from the AGI and parse it. 65 | 66 | :return dict: The AGI response parsed into a dict. 67 | """ 68 | response = await self.reader.readline() 69 | return parse_agi_result(response.decode(self.encoding)[:-1]) 70 | 71 | 72 | class Application(dict): 73 | """Main object: 74 | 75 | .. code-block:: python 76 | 77 | >>> fa_app = Application() 78 | """ 79 | 80 | buf_size = 100 81 | 82 | def __init__( 83 | self, default_encoding='utf-8', loop=None, raise_on_error=False, decode_errors='strict' 84 | ): 85 | super(Application, self).__init__() 86 | self.default_encoding = default_encoding 87 | self.decode_errors = decode_errors 88 | if loop is None: 89 | try: 90 | loop = asyncio.get_running_loop() 91 | except RuntimeError: 92 | loop = asyncio.get_event_loop() 93 | self.loop = loop 94 | self.raise_on_error = raise_on_error 95 | self._route = OrderedDict() 96 | 97 | def add_route(self, path, endpoint): 98 | """Add a route for FastAGI requests: 99 | 100 | :param path: URI to answer. Ex: 'calls/start' 101 | :type path: String 102 | :param endpoint: command to launch. Ex: start 103 | :type endpoint: callable 104 | 105 | :Example: 106 | 107 | :: 108 | 109 | async def start(request): 110 | print('Receive a FastAGI request') 111 | print(['AGI variables:', request.headers]) 112 | 113 | fa_app = Application() 114 | fa_app.add_route('calls/start', start) 115 | 116 | """ 117 | assert callable(endpoint), endpoint 118 | if path in self._route: 119 | raise ValueError('A route already exists.') 120 | if not asyncio.iscoroutinefunction(endpoint): 121 | endpoint = asyncio.coroutine(endpoint) 122 | self._route[path] = endpoint 123 | 124 | def del_route(self, path): 125 | """Delete a route for FastAGI requests: 126 | 127 | :param path: URI to answer. Ex: 'calls/start' 128 | :type path: String 129 | 130 | :Example: 131 | 132 | :: 133 | 134 | async def start(request): 135 | print('Receive a FastAGI request') 136 | print(['AGI variables:', request.headers]) 137 | 138 | fa_app = Application() 139 | fa_app.add_route('calls/start', start) 140 | fa_app.del_route('calls/start') 141 | 142 | """ 143 | if path not in self._route: 144 | raise ValueError('This route doesn\'t exist.') 145 | del self._route[path] 146 | 147 | async def handler(self, reader, writer): 148 | """AsyncIO coroutine handler to launch socket listening. 149 | 150 | :Example: 151 | 152 | :: 153 | 154 | async def start(request): 155 | print('Receive a FastAGI request') 156 | print(['AGI variables:', request.headers]) 157 | 158 | fa_app = Application() 159 | fa_app.add_route('calls/start', start) 160 | coro = asyncio.start_server(fa_app.handler, '0.0.0.0', 4574) 161 | server = loop.run_until_complete(coro) 162 | 163 | See https://docs.python.org/3/library/asyncio-stream.html 164 | """ 165 | buffer = b'' 166 | while b'\n\n' not in buffer: 167 | buffer += await reader.read(self.buf_size) 168 | lines = buffer[:-2].decode(self.default_encoding, errors=self.decode_errors).split('\n') 169 | headers = OrderedDict([ 170 | line.split(': ', 1) for line in lines if ': ' in line 171 | ]) 172 | 173 | agi_network_script = headers.get('agi_network_script') 174 | log.info('Received FastAGI request from %r for "%s" route', 175 | writer.get_extra_info('peername'), agi_network_script) 176 | log.debug("Asterisk Headers: %r", headers) 177 | 178 | if agi_network_script is not None: 179 | route = self._route.get(agi_network_script) 180 | if route is not None: 181 | request = Request(app=self, 182 | headers=headers, 183 | reader=reader, writer=writer, 184 | encoding=self.default_encoding) 185 | try: 186 | await route(request) 187 | except BaseException: 188 | log.exception( 189 | 'An exception has been raised for the request "%s"', 190 | agi_network_script 191 | ) 192 | else: 193 | log.error('No route for the request "%s"', agi_network_script) 194 | else: 195 | log.error('No agi_network_script header for the request') 196 | log.debug("Closing client socket") 197 | writer.close() 198 | -------------------------------------------------------------------------------- /panoramisk/manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | import logging 4 | from collections import defaultdict 5 | from collections import deque 6 | import re 7 | import fnmatch 8 | from .ami_protocol import AMIProtocol 9 | from . import actions 10 | from . import utils 11 | 12 | 13 | class Manager: 14 | """Main object: 15 | 16 | .. code-block:: python 17 | 18 | >>> manager = Manager( 19 | ... host='127.0.0.1', 20 | ... port=5038, 21 | ... ssl=False, 22 | ... encoding='utf8') 23 | """ 24 | 25 | defaults = dict( 26 | host='127.0.0.1', 27 | port=5038, 28 | events='on', 29 | ssl=False, 30 | encoding='utf8', 31 | ping_delay=10, 32 | ping_interval=10, 33 | reconnect_timeout=2, 34 | protocol_factory=AMIProtocol, 35 | save_stream=None, 36 | loop=None, 37 | forgetable_actions=('ping', 'login'), 38 | ) 39 | 40 | def __init__(self, **config): 41 | self.config = dict(self.defaults, **config) 42 | self.loop = self.config['loop'] 43 | self.log = config.get('log', logging.getLogger(__name__)) 44 | self.callbacks = defaultdict(list) 45 | self.protocol = None 46 | self.patterns = [] 47 | self.save_stream = self.config.get('save_stream') 48 | self.authenticated = False 49 | self.authenticated_future = None 50 | self.awaiting_actions = deque() 51 | self.forgetable_actions = self.config['forgetable_actions'] 52 | self.pinger = None 53 | self.ping_delay = int(self.config['ping_delay']) 54 | self.ping_interval = int(self.config['ping_interval']) 55 | self.reconnect_timeout = int(self.config['reconnect_timeout']) 56 | self._connected = False 57 | self.auth_type = self.config.get('auth_type') 58 | self.auth_challenge_future = None 59 | self.register_event('FullyBooted', self.send_awaiting_actions) 60 | self.on_login = config.get('on_login', on_login) 61 | self.on_connect = config.get('on_connect', on_connect) 62 | self.on_disconnect = config.get('on_disconnect', on_disconnect) 63 | 64 | def connection_made(self, f): 65 | if getattr(self, 'protocol', None): 66 | self.protocol.close() 67 | try: 68 | transport, protocol = f.result() 69 | except OSError: # pragma: no cover 70 | if self._connected: 71 | self.log.exception('Not able to connect') 72 | self._connected = False 73 | else: 74 | self.log.warning('Not able to reconnect') 75 | self.loop.call_later(self.reconnect_timeout, self.connect) 76 | else: 77 | self._connected = True 78 | self.log.debug('Manager connected') 79 | self.loop.call_soon(self.on_connect, self) 80 | self.protocol = protocol 81 | self.protocol.queue = deque() 82 | self.protocol.factory = self 83 | self.protocol.log = self.log 84 | self.protocol.config = self.config 85 | self.protocol.encoding = self.encoding = self.config['encoding'] 86 | self.responses = self.protocol.responses = {} 87 | if 'username' in self.config: 88 | if self.auth_type is not None: 89 | if self.auth_type.lower() == 'md5': 90 | self.auth_challenge_future = self.send_action({ 91 | 'Action': 'Challenge', 92 | 'AuthType': self.auth_type.upper()}) 93 | self.auth_challenge_future.add_done_callback(self.secure_login) 94 | return 95 | self.authenticated = False 96 | self.authenticated_future = self.send_action({ 97 | 'Action': 'Login', 98 | 'Username': self.config['username'], 99 | 'Secret': self.config['secret'], 100 | 'Events': self.config['events']}) 101 | self.authenticated_future.add_done_callback(self.login) 102 | else: 103 | self.log.debug('username not in config file') 104 | self.pinger = self.loop.call_later(self.ping_delay, self.ping) 105 | 106 | def secure_login(self, future): 107 | resp = future.result() 108 | if bool(resp.success): 109 | auth_challenge = resp.Challenge + self.config['secret'] 110 | key = hashlib.md5(auth_challenge.encode('utf-8')).hexdigest() 111 | self.authenticated = False 112 | self.authenticated_future = self.send_action({ 113 | 'Action': 'Login', 114 | 'Username': self.config['username'], 115 | 'AuthType': self.auth_type.upper(), 116 | 'Key': key, 117 | 'Events': self.config['events']}) 118 | self.authenticated_future.add_done_callback(self.login) 119 | 120 | def login(self, future): 121 | self.authenticated_future = None 122 | resp = future.result() 123 | self.authenticated = bool(resp.success) 124 | if self.authenticated: 125 | self.loop.call_soon(self.on_login, self) 126 | if self.pinger is not None: 127 | self.pinger.cancel() 128 | self.pinger = self.loop.call_later(self.ping_delay, self.ping) 129 | return self.authenticated 130 | 131 | def ping(self): # pragma: no cover 132 | self.pinger = self.loop.call_later(self.ping_interval, self.ping) 133 | self.protocol.send({'Action': 'Ping'}) 134 | 135 | async def send_awaiting_actions(self, *_): 136 | self.log.info('Sending awaiting actions') 137 | while self.awaiting_actions: 138 | action = self.awaiting_actions.popleft() 139 | if action['action'].lower() not in self.forgetable_actions: 140 | if not action.done(): 141 | self.send_action(action, as_list=action.as_list) 142 | 143 | def send_action(self, action, as_list=None, **kwargs): 144 | """Send an :class:`~panoramisk.actions.Action` to the server: 145 | 146 | :param action: an Action or dict with action name and parameters to 147 | send 148 | :type action: Action or dict or Command 149 | :param as_list: If True, the action will retrieve all responses 150 | :type as_list: boolean 151 | :return: an Action that will receive the response(s) 152 | :rtype: panoramisk.actions.Action 153 | 154 | :Example: 155 | 156 | To retrieve answer:: 157 | 158 | manager = Manager() 159 | resp = await manager.send_action({'Action': 'Status'}) 160 | 161 | Or with an async for:: 162 | 163 | manager = Manager() 164 | async for resp in manager.send_action({'Action': 'Status'}): 165 | print(resp) 166 | 167 | See https://wiki.asterisk.org/wiki/display/AST/AMI+Actions for 168 | more information on actions 169 | """ 170 | action.update(kwargs) 171 | return self.protocol.send(action, as_list=as_list) 172 | 173 | def send_command(self, command, as_list=False): 174 | """Send a :class:`~panoramisk.actions.Command` to the server:: 175 | 176 | manager = Manager() 177 | resp = await manager.send_command('http show status') 178 | 179 | Return a response :class:`~panoramisk.message.Message`. 180 | See https://wiki.asterisk.org/wiki/display/AST/ManagerAction_Command 181 | """ 182 | action = actions.Action({'Command': command, 'Action': 'Command'}, 183 | as_list=as_list) 184 | return self.send_action(action) 185 | 186 | def send_agi_command(self, channel, command, as_list=False): 187 | """Send a :class:`~panoramisk.actions.Command` to the server: 188 | 189 | :param channel: Channel name where to launch command. 190 | Ex: 'SIP/000000-00000a53' 191 | :type channel: String 192 | :param command: command to launch. Ex: 'GET VARIABLE async_agi_server' 193 | :type command: String 194 | :param as_list: If True, the action Future will retrieve all responses 195 | :type as_list: boolean 196 | :return: a Future that will receive the response 197 | :rtype: asyncio.Future 198 | 199 | :Example: 200 | 201 | :: 202 | 203 | manager = Manager() 204 | resp = manager.send_agi_command('SIP/000000-00000a53', 205 | 'GET VARIABLE async_agi_server') 206 | 207 | 208 | Return a response :class:`~panoramisk.message.Message`. 209 | See https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+ManagerAction_AGI 210 | """ 211 | action = actions.Command({'Action': 'AGI', 212 | 'Channel': channel, 213 | 'Command': command}, 214 | as_list=as_list) 215 | return self.send_action(action) 216 | 217 | def connect(self, run_forever=False, on_startup=None, on_shutdown=None): 218 | """connect to the server""" 219 | if self.loop is None: # pragma: no cover 220 | self.loop = asyncio.get_event_loop() 221 | t = asyncio.ensure_future( 222 | self.loop.create_connection( 223 | self.config['protocol_factory'], 224 | self.config['host'], self.config['port'], 225 | ssl=self.config['ssl']), 226 | ) 227 | t.add_done_callback(self.connection_made) 228 | 229 | if run_forever: 230 | self.run_forever(on_startup, on_shutdown) 231 | return t 232 | 233 | def run_forever(self, on_startup, on_shutdown): 234 | """Start loop forever""" 235 | try: 236 | if on_startup: 237 | self.loop.run_until_complete(on_startup(self)) 238 | self.loop.run_forever() 239 | except (KeyboardInterrupt, SystemExit): 240 | self.close() 241 | finally: 242 | if on_shutdown: 243 | self.loop.run_until_complete(on_shutdown(self)) 244 | self.loop.stop() 245 | 246 | def register_event(self, pattern, callback=None): 247 | """register an event. See :class:`~panoramisk.message.Message`: 248 | 249 | .. code-block:: python 250 | 251 | >>> def callback(manager, event): 252 | ... print(manager, event) 253 | >>> manager = Manager() 254 | >>> manager.register_event('Meetme*', callback) 255 | 256 | 257 | You can also use the manager as a decorator: 258 | 259 | .. code-block:: python 260 | 261 | >>> manager = Manager() 262 | >>> @manager.register_event('Meetme*') 263 | ... def callback(manager, event): 264 | ... print(manager, event) 265 | """ 266 | def _register_event(callback): 267 | if not self.callbacks[pattern]: 268 | self.patterns.append((pattern, 269 | re.compile(fnmatch.translate(pattern)))) 270 | self.callbacks[pattern].append(callback) 271 | return callback 272 | if callback is not None: 273 | return _register_event(callback) 274 | else: 275 | return _register_event 276 | 277 | def dispatch(self, event): 278 | matches = [] 279 | event.manager = self 280 | for pattern, regexp in self.patterns: 281 | match = regexp.match(event.event) 282 | if match is not None: 283 | matches.append(pattern) 284 | for callback in self.callbacks[pattern]: 285 | ret = callback(self, event) 286 | if (asyncio.iscoroutine(ret) or 287 | isinstance(ret, asyncio.Future)): 288 | asyncio.ensure_future(ret, loop=self.loop) 289 | return matches 290 | 291 | def close(self): 292 | """Close the connection""" 293 | if self.pinger: 294 | self.pinger.cancel() 295 | self.pinger = None 296 | if getattr(self, 'protocol', None): 297 | self.protocol.close() 298 | 299 | def connection_lost(self, exc): 300 | self._connected = False 301 | self.log.error('Connection lost') 302 | self.loop.call_soon(self.on_disconnect, self, exc) 303 | if self.pinger: 304 | self.pinger.cancel() 305 | self.pinger = None 306 | self.log.info('Try to connect again in %d second(s)' % self.reconnect_timeout) 307 | self.loop.call_later(self.reconnect_timeout, self.connect) 308 | 309 | @classmethod 310 | def from_config(cls, filename_or_fd, section='asterisk', **kwargs): 311 | config = utils.config(filename_or_fd, section=section) 312 | config.update(kwargs) 313 | return cls(**config) 314 | 315 | 316 | # noinspection PyUnusedLocal 317 | def on_connect(manager: Manager): 318 | """ 319 | Callback after connect 320 | """ 321 | pass 322 | 323 | 324 | # noinspection PyUnusedLocal 325 | def on_login(manager: Manager): 326 | """ 327 | Callback after login 328 | """ 329 | pass 330 | 331 | 332 | # noinspection PyUnusedLocal 333 | def on_disconnect(manager: Manager, exc: Exception): 334 | """ 335 | Callback after disconnect 336 | """ 337 | pass 338 | -------------------------------------------------------------------------------- /panoramisk/message.py: -------------------------------------------------------------------------------- 1 | from . import utils 2 | from urllib.parse import unquote 3 | 4 | 5 | class Message(utils.CaseInsensitiveDict): 6 | """Handle both Responses and Events with the same api: 7 | 8 | .. 9 | >>> resp = Message({'Response': 'Follows'}, 'Response body') 10 | >>> event = Message({'Event': 'MeetmeEnd', 'Meetme': '4242'}) 11 | 12 | Responses: 13 | 14 | .. code-block:: python 15 | 16 | >>> bool(resp.success) 17 | True 18 | >>> resp 19 | 20 | >>> print(resp.content) 21 | Response body 22 | >>> for line in resp.iter_lines(): 23 | ... print(resp.content) 24 | Response body 25 | 26 | Events: 27 | 28 | .. code-block:: python 29 | 30 | >>> print(event['meetme']) 31 | 4242 32 | >>> print(event.meetme) 33 | 4242 34 | >>> event.unknown_header 35 | '' 36 | 37 | """ 38 | 39 | quoted_keys = ['result'] 40 | success_responses = ['Success', 'Follows', 'Goodbye'] 41 | 42 | def __init__(self, headers, content=''): 43 | super(Message, self).__init__(headers, content=content) 44 | self.manager = None 45 | 46 | @property 47 | def id(self): 48 | if 'commandid' in self: 49 | return self['commandid'] 50 | elif 'actionid' in self: 51 | return self['actionid'] 52 | return None 53 | 54 | @property 55 | def action_id(self): 56 | if 'actionid' in self: 57 | return self['actionid'] 58 | return None 59 | 60 | @property 61 | def success(self): 62 | """return True if a response status is Success or Follows: 63 | 64 | .. code-block:: python 65 | 66 | >>> resp = Message({'Response': 'Success'}) 67 | >>> print(resp.success) 68 | True 69 | >>> resp['Response'] = 'Failed' 70 | >>> resp.success 71 | False 72 | """ 73 | if 'event' in self: 74 | return True 75 | if self.response in self.success_responses: 76 | return True 77 | return False 78 | 79 | def __repr__(self): 80 | message = ' '.join(['%s=%r' % i for i in sorted(self.items())]) 81 | return ''.format(message) 82 | 83 | def iter_lines(self): 84 | """Iter over response body""" 85 | for line in self.content.split('\n'): 86 | yield line 87 | 88 | def parsed_result(self): 89 | """Get parsed result of AGI command""" 90 | if 'Result' in self: 91 | return utils.parse_agi_result(self['Result']) 92 | else: 93 | raise ValueError('No result in %r' % self) 94 | 95 | def getdict(self, key): 96 | """Convert a multi values header to a case-insensitive dict: 97 | 98 | .. code-block:: python 99 | 100 | >>> resp = Message({ 101 | ... 'Response': 'Success', 102 | ... 'ChanVariable': [ 103 | ... 'FROM_DID=', 'SIPURI=sip:42@10.10.10.1:4242'], 104 | ... }) 105 | >>> print(resp.chanvariable) 106 | ['FROM_DID=', 'SIPURI=sip:42@10.10.10.1:4242'] 107 | >>> value = resp.getdict('chanvariable') 108 | >>> print(value['sipuri']) 109 | sip:42@10.10.10.1:4242 110 | """ 111 | values = self.get(key, None) 112 | result = utils.CaseInsensitiveDict() 113 | if not isinstance(values, list): 114 | result[key] = values 115 | return result 116 | for item in values: 117 | k, v = item.split('=', 1) 118 | result[k] = v 119 | return result 120 | 121 | @classmethod 122 | def from_line(cls, line): 123 | mlines = line.split(utils.EOL) 124 | headers = {} 125 | content = '' 126 | has_body = ('Response: Follows', 'Response: Fail') 127 | if mlines[0].startswith(has_body): 128 | content = mlines.pop() 129 | while not content and mlines: 130 | content = mlines.pop() 131 | for mline in mlines: 132 | if ': ' in mline: 133 | k, v = mline.split(': ', 1) 134 | if k.lower() in cls.quoted_keys: 135 | v = unquote(v).strip() 136 | if k in headers: 137 | o = headers.setdefault(k, []) 138 | if not isinstance(o, list): 139 | o = [o] 140 | o.append(v) 141 | headers[k] = o 142 | else: 143 | headers[k] = v 144 | if 'Event' in headers or 'Response' in headers: 145 | return cls(headers, content) 146 | -------------------------------------------------------------------------------- /panoramisk/testing.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import asyncio 3 | from unittest import mock 4 | 5 | from . import manager 6 | from . import utils 7 | 8 | 9 | MagicMock = mock.MagicMock 10 | patch = mock.patch 11 | call = mock.call 12 | 13 | 14 | class AMIProtocol(manager.AMIProtocol): 15 | 16 | debug_count = [0] 17 | 18 | def connection_made(self, transport): 19 | super(AMIProtocol, self).connection_made(transport) 20 | self.transport = MagicMock() 21 | 22 | def send(self, data, as_list=False): 23 | utils.IdGenerator.reset(uid='transaction_uid') 24 | future = super(AMIProtocol, self).send(data, as_list=as_list) 25 | if getattr(self.factory, 'stream', None) is not None: 26 | with open(self.factory.stream, 'rb') as fd: 27 | for resp in fd.read().split(b'\n\n'): 28 | self.data_received(resp + b'\n\n') 29 | if future.done(): 30 | break 31 | if not future.done(): # pragma: no cover 32 | print(self.responses) 33 | raise AssertionError("Future's result was never set") 34 | return future 35 | 36 | 37 | class Manager(manager.Manager): 38 | 39 | fixtures_dir = None 40 | defaults = manager.Manager.defaults.copy() 41 | 42 | def __init__(self, **config): 43 | self.defaults.update( 44 | protocol_factory=AMIProtocol, 45 | stream=None) 46 | super(Manager, self).__init__(**config) 47 | 48 | self.stream = self.config.get('stream') 49 | 50 | if self.loop is None: 51 | self.loop = asyncio.get_event_loop() 52 | 53 | protocol = AMIProtocol() 54 | protocol.factory = manager 55 | protocol.connection_made(mock.MagicMock()) 56 | future = self.loop.create_future() 57 | future.set_result((mock.MagicMock(), protocol)) 58 | self.protocol = protocol 59 | self.connection_made(future) 60 | 61 | utils.IdGenerator.reset(uid='transaction_uid') 62 | utils.EOL = '\n' 63 | -------------------------------------------------------------------------------- /panoramisk/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import uuid 3 | 4 | try: 5 | from collections.abc import MutableMapping 6 | except ImportError: 7 | from collections import MutableMapping 8 | 9 | 10 | from configparser import ConfigParser 11 | from .exceptions import (AGIAppError, AGIDeadChannelError, AGIInvalidCommand, AGINoResultError, 12 | AGIResultHangup, AGIUnknownError, AGIUsageError) 13 | 14 | EOL = '\r\n' 15 | 16 | re_code = re.compile(r'(?P^\d*)\s*(?P.*)') 17 | re_kv = re.compile(r'(?P\w+)=(?P[^\s]*)\s*(?:\((?P.*)\))*') 18 | 19 | 20 | def parse_agi_result(line): 21 | """Parse AGI results using Regular expression. 22 | 23 | AGI Result examples:: 24 | 25 | 100 result=0 Trying... 26 | 27 | 200 result=0 28 | 29 | 200 result=-1 30 | 31 | 200 result=132456 32 | 33 | 200 result= (timeout) 34 | 35 | 510 Invalid or unknown command 36 | 37 | 520-Invalid command syntax. Proper usage follows: 38 | int() argument must be a string, a bytes-like object or a number, not 39 | 'NoneType' 40 | 41 | HANGUP 42 | 43 | """ 44 | 45 | if line == 'HANGUP': 46 | raise AGIResultHangup('User hungup during execution', {}) 47 | 48 | match = re_code.search(line) 49 | # re_code matches on any string, no need to check 50 | code = match.groupdict().get('code') or 0 51 | response = match.groupdict().get('response', '') 52 | return agi_code_check(code, response, line) 53 | 54 | 55 | def agi_code_check(code, response, line): 56 | """ 57 | Check the AGI code and return a dict to help on error handling. 58 | """ 59 | code = int(code) 60 | response = response or '' 61 | result = {'status_code': code, 'msg': ''} 62 | if code == 100: 63 | pass # Trying 64 | elif code == 200: 65 | for key, value, data in re_kv.findall(response): 66 | result[key] = (value, data) 67 | # If user hangs up... we get 'hangup' in the data 68 | if data == 'hangup': 69 | raise AGIResultHangup('User hungup during execution', result) 70 | if key == 'result' and value == '-1': 71 | raise AGIAppError('Error executing application, or hangup', result) 72 | if 'result' not in result: # Result must be present 73 | raise AGINoResultError('Result key/value pair missing in Asterisk response', result) 74 | elif code == 510: 75 | raise AGIInvalidCommand(line, result) 76 | elif code == 511: 77 | raise AGIDeadChannelError(line, result) 78 | elif code == 520: 79 | raise AGIUsageError(line, result) 80 | else: 81 | # Unhandled code or undefined response 82 | raise AGIUnknownError(line, result) 83 | 84 | return result 85 | 86 | 87 | class IdGenerator: 88 | """Generate some uuid for actions: 89 | 90 | .. code-block:: python 91 | 92 | >>> g = IdGenerator('mycounter') 93 | 94 | .. 95 | >>> IdGenerator.reset(uid='an_uuid4') 96 | 97 | It increments the counter at each calls: 98 | 99 | .. code-block:: python 100 | 101 | >>> print(g()) 102 | mycounter/an_uuid4/1/1 103 | >>> print(g()) 104 | mycounter/an_uuid4/1/2 105 | """ 106 | 107 | instances = [] 108 | 109 | def __init__(self, prefix): 110 | self.instances.append(self) 111 | self.prefix = prefix 112 | self.uid = str(uuid.uuid4()) 113 | self.generator = self.get_generator() 114 | 115 | def get_generator(self): 116 | i = 0 117 | max_val = 10000 118 | while True: 119 | yield "%s/%s/%d/%d" % (self.prefix, 120 | self.uid, (i // max_val) + 1, 121 | (i % max_val) + 1) 122 | i += 1 123 | 124 | @classmethod 125 | def reset(cls, uid=None): 126 | """Mostly used for unit testing. Allow to use a static uuid and reset 127 | all counter""" 128 | for instance in cls.instances: 129 | if uid: 130 | instance.uid = uid 131 | instance.generator = instance.get_generator() 132 | 133 | def get_instances(self): 134 | """Mostly used for debugging""" 135 | return ["<%s prefix:%s (uid:%s)>" % (self.__class__.__name__, 136 | i.prefix, self.uid) 137 | for i in self.instances] 138 | 139 | def __call__(self): 140 | return next(self.generator) 141 | 142 | def __repr__(self): 143 | return "<%s prefix:%s (uid:%s)>" % (self.__class__.__name__, self.prefix, self.uid) 144 | 145 | 146 | class CaseInsensitiveDict(MutableMapping): 147 | """ 148 | A case-insensitive ``dict``-like object. 149 | 150 | Implements all methods and operations of ``collections.MutableMapping``. 151 | 152 | All keys are expected to be strings. The structure remembers the 153 | case of the last key to be set, and ``iter(instance)``, 154 | ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` 155 | will contain case-sensitive keys. However, querying and contains 156 | testing is case insensitive: 157 | 158 | .. code-block:: python 159 | 160 | cid = CaseInsensitiveDict() 161 | cid['Action'] = 'SIPnotify' 162 | cid['aCTION'] == 'SIPnotify' # True 163 | list(cid) == ['Action'] # True 164 | 165 | For example, ``event['actionid']`` will return the 166 | value of a ``'ActionID'`` response event, regardless 167 | of how the event name was originally stored. 168 | """ 169 | def __init__(self, data=None, **kwargs): 170 | self._store = dict() 171 | self.update(data or {}, **kwargs) 172 | 173 | def __setitem__(self, key, value): 174 | # Use the lowercased key for lookups, but store the actual 175 | # key alongside the value. 176 | self._store[key.lower()] = (key, value) 177 | 178 | def __contains__(self, key): 179 | return key.lower() in self._store 180 | 181 | def __getattr__(self, attr): 182 | return self.get(attr, '') 183 | 184 | def __getitem__(self, key): 185 | return self._store[key.lower()][1] 186 | 187 | def __delitem__(self, key): 188 | raise NotImplementedError() 189 | 190 | def __iter__(self): 191 | return (key for key, value in self._store.values()) 192 | 193 | def __len__(self): 194 | return len(self._store) 195 | 196 | def __repr__(self): 197 | return str(dict(self.items())) 198 | 199 | 200 | def config(filename_or_fd, section='asterisk'): 201 | config = ConfigParser() 202 | if hasattr(filename_or_fd, 'read'): 203 | if hasattr(config, 'read_file'): 204 | config.read_file(filename_or_fd) 205 | else: 206 | config.readfp(filename_or_fd) 207 | else: 208 | config.read(filename_or_fd) 209 | return dict(config.items(section)) 210 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --doctest-modules 3 | --doctest-glob='*.rst' 4 | --ignore=setup.py 5 | --ignore=bootstrap.py 6 | --ignore=docs/conf.py 7 | --ignore=panoramisk/testing.py 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from setuptools import setup 5 | from setuptools import find_packages 6 | 7 | version = '1.5.dev0' 8 | 9 | install_requires = [] 10 | test_requires = [ 11 | 'pytest', 12 | 'pytest-asyncio', 13 | 'coverage', 14 | 'coveralls', 15 | ] 16 | 17 | 18 | def read(*rnames): 19 | return open(os.path.join(os.path.dirname(__file__), *rnames)).read() 20 | 21 | 22 | setup( 23 | name='panoramisk', 24 | version=version, 25 | description="asyncio based library to play with asterisk", 26 | long_description=read('README.rst'), 27 | classifiers=[ 28 | 'Development Status :: 5 - Production/Stable', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | 'Programming Language :: Python :: 3.9', 36 | 'Programming Language :: Python :: 3.10', 37 | 'Programming Language :: Python :: 3.11', 38 | 'Programming Language :: Python :: 3.12', 39 | 'Topic :: Communications :: Telephony', 40 | 'Topic :: Software Development :: Libraries :: Python Modules', 41 | ], 42 | keywords=['asyncio', 'asterisk', 'voip'], 43 | author='Gael Pasgrimaud', 44 | author_email='gael@gawel.org', 45 | url='https://github.com/gawel/panoramisk/', 46 | license='MIT license', 47 | packages=find_packages(exclude=['docs', 'tests']), 48 | include_package_data=True, 49 | zip_safe=False, 50 | install_requires=install_requires, 51 | tests_require=test_requires, 52 | extras_require={ 53 | 'test': test_requires, 54 | }, 55 | entry_points=''' 56 | [console_scripts] 57 | panoramisk = panoramisk.command:main 58 | ''' 59 | ) 60 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gawel/panoramisk/d0df36b0ef1e88abecad22b3f9b68d6384be31b6/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import pytest 4 | 5 | from panoramisk import utils 6 | 7 | 8 | class Asterisk: 9 | def __init__(self): 10 | self.cwd = 'tests/docker' 11 | self.proc = None 12 | 13 | def start(self): 14 | self.stop() 15 | self.proc = subprocess.Popen( 16 | ['docker-compose', 'up'], 17 | stdout=subprocess.PIPE, 18 | stderr=subprocess.STDOUT, 19 | cwd=self.cwd 20 | ) 21 | for line in iter(self.proc.stdout.readline, b''): 22 | if b'Asterisk Ready.' in line: 23 | break 24 | 25 | def logs(self, tail=20): 26 | proc = subprocess.Popen( 27 | ['docker-compose', 'logs', '--tail=%s' % tail], 28 | stdout=subprocess.PIPE, 29 | stderr=subprocess.STDOUT, 30 | cwd=self.cwd, 31 | encoding='utf8', 32 | ) 33 | stdout, _ = proc.communicate() 34 | print(stdout) 35 | return stdout 36 | 37 | def stop(self): 38 | if self.proc is not None: 39 | self.proc.kill() 40 | subprocess.check_call( 41 | ['docker-compose', 'down', '-v'], 42 | stdout=subprocess.PIPE, 43 | stderr=subprocess.STDOUT, 44 | cwd=self.cwd, 45 | ) 46 | 47 | 48 | @pytest.fixture 49 | def asterisk(request): 50 | utils.EOL = '\r\n' 51 | server = Asterisk() 52 | yield server 53 | server.stop() 54 | -------------------------------------------------------------------------------- /tests/docker/asterisk-cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker-compose exec asterisk asterisk -rc 4 | -------------------------------------------------------------------------------- /tests/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: "3" 4 | 5 | services: 6 | 7 | asterisk: 8 | image: andrius/asterisk:latest 9 | ports: 10 | - 5038:5038 11 | - 5060:5060/udp 12 | - 5060:5060/tcp 13 | volumes: 14 | - ./:/opt 15 | entrypoint: /opt/docker-entrypoint.sh 16 | -------------------------------------------------------------------------------- /tests/docker/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | # run as user asterisk by default 6 | ASTERISK_USER=${ASTERISK_USER:-asterisk} 7 | ASTERISK_GROUP=${ASTERISK_GROUP:-${ASTERISK_USER}} 8 | 9 | if [ "$1" = "" ]; then 10 | COMMAND="/usr/sbin/asterisk -T -W -U ${ASTERISK_USER} -p -vvvdddf" 11 | cp /opt/etc/* /etc/asterisk/ 12 | else 13 | COMMAND="$@" 14 | fi 15 | 16 | if [ "${ASTERISK_UID}" != "" ] && [ "${ASTERISK_GID}" != "" ]; then 17 | # recreate user and group for asterisk 18 | # if they've sent as env variables (i.e. to macth with host user to fix permissions for mounted folders 19 | 20 | deluser asterisk && \ 21 | addgroup -g ${ASTERISK_GID} ${ASTERISK_GROUP} && \ 22 | adduser -D -H -u ${ASTERISK_UID} -G ${ASTERISK_GROUP} ${ASTERISK_USER} \ 23 | || exit 24 | fi 25 | 26 | chown -R ${ASTERISK_USER}: /etc/asterisk \ 27 | /var/log/asterisk \ 28 | /var/lib/asterisk \ 29 | /var/run/asterisk \ 30 | /var/spool/asterisk; \ 31 | exec ${COMMAND} 32 | -------------------------------------------------------------------------------- /tests/docker/etc/manager.conf: -------------------------------------------------------------------------------- 1 | ; 2 | ; AMI - The Asterisk Manager Interface 3 | ; 4 | ; Third party application call management support and PBX event supervision 5 | ; 6 | ; Use the "manager show commands" at the CLI to list available manager commands 7 | ; and their authorization levels. 8 | ; 9 | ; "manager show command " will show a help text. 10 | ; 11 | ; ---------------------------- SECURITY NOTE ------------------------------- 12 | ; Note that you should not enable the AMI on a public IP address. If needed, 13 | ; block this TCP port with iptables (or another FW software) and reach it 14 | ; with IPsec, SSH, or SSL vpn tunnel. You can also make the manager 15 | ; interface available over http/https if Asterisk's http server is enabled in 16 | ; http.conf and if both "enabled" and "webenabled" are set to yes in 17 | ; this file. Both default to no. httptimeout provides the maximum 18 | ; timeout in seconds before a web based session is discarded. The 19 | ; default is 60 seconds. 20 | ; 21 | [general] 22 | enabled = yes 23 | ;webenabled = yes 24 | 25 | port = 5038 26 | bindaddr = 0.0.0.0 27 | 28 | ; Parameters that control AMI over TLS. ("enabled" must be set too). 29 | ; You can open a connection to this socket with e.g. 30 | ; 31 | ; openssl s_client -connect my_host:5039 32 | ; 33 | ;tlsenable=no ; set to YES to enable it 34 | ;tlsbindaddr=0.0.0.0:5039 ; address and port to bind to, default to bindaddr and port 5039 35 | ;tlscertfile=/tmp/asterisk.pem ; path to the certificate. 36 | ;tlsprivatekey=/tmp/private.pem ; path to the private key, if no private given, 37 | ; if no tlsprivatekey is given, default is to search 38 | ; tlscertfile for private key. 39 | ;tlscipher= ; string specifying which SSL ciphers to use or not use 40 | ; 41 | ;allowmultiplelogin = yes ; IF set to no, rejects manager logins that are already in use. 42 | ; ; The default is yes. 43 | ; 44 | ;displayconnects = yes 45 | ; 46 | ; Add a Unix epoch timestamp to events (not action responses) 47 | ; 48 | ;timestampevents = yes 49 | 50 | ;brokeneventsaction = yes ; Restore previous behavior that caused the events 51 | ; action to not return a response in certain 52 | ; circumstances. Defaults to 'no'. 53 | 54 | ; 55 | ; Display certain channel variables every time a channel-oriented 56 | ; event is emitted: 57 | ; 58 | ; Note that this does incur a performance penalty and should be avoided if possible. 59 | ; 60 | ;channelvars = var1,var2,var3 61 | 62 | debug = on ; enable some debugging info in AMI messages (default off). 63 | ; Also accessible through the "manager debug" CLI command. 64 | 65 | ; authtimeout specifies the maximum number of seconds a client has to 66 | ; authenticate. If the client does not authenticate beofre this timeout 67 | ; expires, the client will be disconnected. (default: 30 seconds) 68 | 69 | ;authtimeout = 30 70 | 71 | ; authlimit specifies the maximum number of unauthenticated sessions that will 72 | ; be allowed to connect at any given time. 73 | 74 | ;authlimit = 50 75 | 76 | ;httptimeout = 60 77 | ; a) httptimeout sets the Max-Age of the http cookie 78 | ; b) httptimeout is the amount of time the webserver waits 79 | ; on a action=waitevent request (actually its httptimeout-10) 80 | ; c) httptimeout is also the amount of time the webserver keeps 81 | ; a http session alive after completing a successful action 82 | 83 | 84 | [username] 85 | secret = mysecret 86 | ;deny=0.0.0.0/0.0.0.0 87 | ;permit=209.16.236.73/255.255.255.0 88 | ;acl=named_acl_example ; use a named ACL from acl.conf 89 | ; 90 | ; 91 | ;setvar=PBXACCOUNT=edvina 92 | ; The setvar option defines channel variables that will be set when this account 93 | ; originates a call. You can define multiple setvar= commands for one manager 94 | ; user. 95 | ; 96 | ;eventfilter=Event: Newchannel 97 | ;eventfilter=Channel: (PJ)?SIP/(james|jim|john)- 98 | ;eventfilter=!Channel: DAHDI/ 99 | ; The eventfilter option is used to whitelist or blacklist events per user. 100 | ; A filter consists of an (unanchored) regular expression that is run on the 101 | ; entire event data. If the first character of the filter is an exclamation 102 | ; mark (!), the filter is appended to the blacklist instead of the whitelist. 103 | ; After first checking the read access below, the regular expression filters 104 | ; are processed as follows: 105 | ; - If no filters are configured all events are reported as normal. 106 | ; - If there are white filters only: implied black all filter processed first, 107 | ; then white filters. 108 | ; - If there are black filters only: implied white all filter processed first, 109 | ; then black filters. 110 | ; - If there are both white and black filters: implied black all filter processed 111 | ; first, then white filters, and lastly black filters. 112 | 113 | ; 114 | ; If the device connected via this user accepts input slowly, 115 | ; the timeout for writes to it can be increased to keep it 116 | ; from being disconnected (value is in milliseconds) 117 | ; 118 | ; writetimeout = 100 119 | ; 120 | ;displayconnects = yes ; Display on CLI user login/logoff 121 | ; 122 | ; Authorization for various classes 123 | ; 124 | ; Read authorization permits you to receive asynchronous events, in general. 125 | ; Write authorization permits you to send commands and get back responses. The 126 | ; following classes exist: 127 | ; 128 | ; all - All event classes below (including any we may have missed). 129 | ; system - General information about the system and ability to run system 130 | ; management commands, such as Shutdown, Restart, and Reload. This 131 | ; class also includes dialplan manipulation actions such as 132 | ; DialplanExtensionAdd and DialplanExtensionRemove. 133 | ; call - Information about channels and ability to set information in a 134 | ; running channel. 135 | ; log - Logging information. Read-only. (Defined but not yet used.) 136 | ; verbose - Verbose information. Read-only. (Defined but not yet used.) 137 | ; agent - Information about queues and agents and ability to add queue 138 | ; members to a queue. 139 | ; user - Permission to send and receive UserEvent. 140 | ; config - Ability to read and write configuration files. 141 | ; command - Permission to run CLI commands. Write-only. 142 | ; dtmf - Receive DTMF events. Read-only. 143 | ; reporting - Ability to get information about the system. 144 | ; cdr - Output of cdr_manager, if loaded. Read-only. 145 | ; dialplan - Receive NewExten and VarSet events. Read-only. 146 | ; originate - Permission to originate new calls. Write-only. 147 | ; agi - Output AGI commands executed. Input AGI command to execute. 148 | ; cc - Call Completion events. Read-only. 149 | ; aoc - Permission to send Advice Of Charge messages and receive Advice 150 | ; - Of Charge events. 151 | ; test - Ability to read TestEvent notifications sent to the Asterisk Test 152 | ; Suite. Note that this is only enabled when the TEST_FRAMEWORK 153 | ; compiler flag is defined. 154 | ; security - Security Events. Read-only. 155 | ; message - Permissions to send out of call messages. Write-only 156 | ; 157 | read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan 158 | write = system,call,agent,user,config,command,reporting,originate,message 159 | -------------------------------------------------------------------------------- /tests/fixtures/agent_loggued_in.yaml: -------------------------------------------------------------------------------- 1 | 2 | Action: Ping 3 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/22 4 | 5 | Response: Success 6 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/22 7 | Ping: Pong 8 | Timestamp: 1409169929.412068 9 | 10 | Action: Ping 11 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/23 12 | 13 | Response: Success 14 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/23 15 | Ping: Pong 16 | Timestamp: 1409169939.419909 17 | 18 | Action: Ping 19 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/24 20 | 21 | Response: Success 22 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/24 23 | Ping: Pong 24 | Timestamp: 1409169949.427993 25 | 26 | Event: PeerStatus 27 | Privilege: system,all 28 | ChannelType: SIP 29 | Peer: SIP/hhhhh 30 | PeerStatus: Registered 31 | Address: 192.168.1.5:5060 32 | 33 | Event: QueueMemberStatus 34 | Privilege: agent,all 35 | Queue: general_queue 36 | Location: Local/id-1@agentcallback 37 | MemberName: Agent/401 38 | StateInterface: SIP/hhhhh 39 | Membership: dynamic 40 | Penalty: 0 41 | CallsTaken: 0 42 | LastCall: 0 43 | Status: 1 44 | Paused: 0 45 | Skills: agent-1 46 | 47 | Event: PeerStatus 48 | Privilege: system,all 49 | ChannelType: SIP 50 | Peer: SIP/999999 51 | PeerStatus: Registered 52 | Address: 192.168.1.5:5060 53 | 54 | Action: Ping 55 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/25 56 | 57 | Response: Success 58 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/25 59 | Ping: Pong 60 | Timestamp: 1409169959.432195 61 | 62 | Action: Ping 63 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/26 64 | 65 | Response: Success 66 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/26 67 | Ping: Pong 68 | Timestamp: 1409169969.445850 69 | 70 | Action: Ping 71 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/27 72 | 73 | Response: Success 74 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/27 75 | Ping: Pong 76 | Timestamp: 1409169979.455934 77 | 78 | Event: Newchannel 79 | Privilege: call,all 80 | Channel: SIP/999999-00000002 81 | ChannelState: 0 82 | ChannelStateDesc: Down 83 | CallerIDNum: 202 84 | CallerIDName: user 202 85 | AccountCode: 86 | Exten: *30402 87 | Context: default 88 | Uniqueid: 1409169987.2 89 | 90 | Event: VarSet 91 | Privilege: dialplan,all 92 | Channel: SIP/999999-00000002 93 | Variable: SIPURI 94 | Value: sip:999999@192.168.1.5:5060 95 | Uniqueid: 1409169987.2 96 | 97 | Event: VarSet 98 | Privilege: dialplan,all 99 | Channel: SIP/999999-00000002 100 | Variable: SIPDOMAIN 101 | Value: 10.174.0.1 102 | Uniqueid: 1409169987.2 103 | 104 | Event: VarSet 105 | Privilege: dialplan,all 106 | Channel: SIP/999999-00000002 107 | Variable: SIPCALLID 108 | Value: fa2d0521-75ad-43a9-8dd9-a99a0dae3d26 109 | Uniqueid: 1409169987.2 110 | 111 | Event: VarSet 112 | Privilege: dialplan,all 113 | Channel: SIP/999999-00000002 114 | Variable: XIVO_USERID 115 | Value: 2 116 | Uniqueid: 1409169987.2 117 | 118 | Event: VarSet 119 | Privilege: dialplan,all 120 | Channel: SIP/999999-00000002 121 | Variable: PICKUPMARK 122 | Value: 202%default 123 | Uniqueid: 1409169987.2 124 | 125 | Event: VarSet 126 | Privilege: dialplan,all 127 | Channel: SIP/999999-00000002 128 | Variable: TRANSFER_CONTEXT 129 | Value: default 130 | Uniqueid: 1409169987.2 131 | 132 | Event: ChannelUpdate 133 | Privilege: system,all 134 | Channel: SIP/999999-00000002 135 | Uniqueid: 1409169987.2 136 | Channeltype: SIP 137 | SIPcallid: fa2d0521-75ad-43a9-8dd9-a99a0dae3d26 138 | SIPfullcontact: sip:999999@192.168.1.5:5060 139 | 140 | [218 bytes missing in capture file]Event: Newexten 141 | Privilege: dialplan,all 142 | Channel: SIP/999999-00000002 143 | Context: default 144 | Extension: *30402 145 | Priority: 1 146 | Application: Set 147 | AppData: XIVO_BASE_CONTEXT=default 148 | Uniqueid: 1409169987.2 149 | 150 | Event: VarSet 151 | Privilege: dialplan,all 152 | Channel: SIP/999999-00000002 153 | Variable: XIVO_BASE_CONTEXT 154 | Value: default 155 | Uniqueid: 1409169987.2 156 | 157 | Event: Newexten 158 | Privilege: dialplan,all 159 | Channel: SIP/999999-00000002 160 | Context: default 161 | Extension: *30402 162 | Priority: 2 163 | Application: Set 164 | AppData: XIVO_BASE_EXTEN=*30402 165 | Uniqueid: 1409169987.2 166 | 167 | Event: VarSet 168 | Privilege: dialplan,all 169 | Channel: SIP/999999-00000002 170 | Variable: XIVO_BASE_EXTEN 171 | Value: *30402 172 | Uniqueid: 1409169987.2 173 | 174 | Event: Newexten 175 | Privilege: dialplan,all 176 | Channel: SIP/999999-00000002 177 | Context: default 178 | Extension: *30402 179 | Priority: 3 180 | Application: Gosub 181 | AppData: agentstaticlogtoggle,s,1(402) 182 | Uniqueid: 1409169987.2 183 | 184 | Event: VarSet 185 | Privilege: dialplan,all 186 | Channel: SIP/999999-00000002 187 | Variable: LOCAL(ARG1) 188 | Value: 402 189 | Uniqueid: 1409169987.2 190 | 191 | Event: VarSet 192 | Privilege: dialplan,all 193 | Channel: SIP/999999-00000002 194 | Variable: LOCAL(ARGC) 195 | Value: 1 196 | Uniqueid: 1409169987.2 197 | 198 | Event: Newexten 199 | Privilege: dialplan,all 200 | Channel: SIP/999999-00000002 201 | Context: agentstaticlogtoggle 202 | Extension: s 203 | Priority: 1 204 | Application: NoOp 205 | AppData: 206 | Uniqueid: 1409169987.2 207 | 208 | Event: CEL 209 | Privilege: call,all 210 | EventName: CHAN_START 211 | AccountCode: 212 | CallerIDnum: 202 213 | CallerIDname: user 202 214 | CallerIDani: 215 | CallerIDrdnis: 216 | CallerIDdnid: 217 | Exten: *30402 218 | Context: default 219 | Channel: SIP/999999-00000002 220 | Application: 221 | AppData: 222 | EventTime: 2014-08-27 21:06:27 223 | AMAFlags: DOCUMENTATION 224 | UniqueID: 1409169987.2 225 | LinkedID: 1409169987.2 226 | Userfield: 227 | Peer: 228 | PeerAccount: 229 | Extra: 230 | 231 | Event: ExtensionStatus 232 | Privilege: call,all 233 | Exten: 202 234 | Context: default 235 | Hint: SIP/999999 236 | Status: 1 237 | 238 | Event: ExtensionStatus 239 | Privilege: call,all 240 | Exten: *6662 241 | Context: default 242 | Hint: SIP/999999 243 | Status: 1 244 | 245 | Event: Newexten 246 | Privilege: dialplan,all 247 | Channel: SIP/999999-00000002 248 | Context: xivo-pickup 249 | Extension: pickup 250 | Priority: 3 251 | Application: Wait 252 | AppData: 1 253 | Uniqueid: 1409169987.2 254 | 255 | Event: CEL 256 | Privilege: call,all 257 | EventName: ANSWER 258 | AccountCode: 259 | CallerIDnum: 202 260 | CallerIDname: user 202 261 | CallerIDani: 202 262 | CallerIDrdnis: 263 | CallerIDdnid: *30402 264 | Exten: pickup 265 | Context: xivo-pickup 266 | Channel: SIP/999999-00000002 267 | Application: Answer 268 | AppData: 269 | EventTime: 2014-08-27 21:06:27 270 | AMAFlags: DOCUMENTATION 271 | UniqueID: 1409169987.2 272 | LinkedID: 1409169987.2 273 | Userfield: 274 | Peer: 275 | PeerAccount: 276 | Extra: 277 | 278 | Event: Newexten 279 | Privilege: dialplan,all 280 | Channel: SIP/999999-00000002 281 | Context: xivo-pickup 282 | Extension: pickup 283 | Priority: 4 284 | Application: Set 285 | AppData: XIVO_PICKEDUP=1 286 | Uniqueid: 1409169987.2 287 | 288 | Event: VarSet 289 | Privilege: dialplan,all 290 | Channel: SIP/999999-00000002 291 | Variable: XIVO_PICKEDUP 292 | Value: 1 293 | Uniqueid: 1409169987.2 294 | 295 | Event: Newexten 296 | Privilege: dialplan,all 297 | Channel: SIP/999999-00000002 298 | Context: xivo-pickup 299 | Extension: pickup 300 | Priority: 5 301 | Application: Return 302 | AppData: 303 | Uniqueid: 1409169987.2 304 | 305 | Event: VarSet 306 | Privilege: dialplan,all 307 | Channel: SIP/999999-00000002 308 | Variable: GOSUB_RETVAL 309 | Value: 310 | Uniqueid: 1409169987.2 311 | 312 | Event: Newexten 313 | Privilege: dialplan,all 314 | Channel: SIP/999999-00000002 315 | Context: agentstaticlogtoggle 316 | Extension: s 317 | Priority: 4 318 | Application: AGI 319 | AppData: agi://127.0.0.1/agent_get_options,402 320 | Uniqueid: 1409169987.2 321 | 322 | Event: AGIExec 323 | Privilege: agi,all 324 | SubEvent: Start 325 | Channel: SIP/999999-00000002 326 | CommandId: 1866797246 327 | Command: SET VARIABLE "XIVO_AGENTEXISTS" "0" 328 | 329 | Event: VarSet 330 | Privilege: dialplan,all 331 | Channel: SIP/999999-00000002 332 | Variable: XIVO_AGENTEXISTS 333 | Value: 0 334 | Uniqueid: 1409169987.2 335 | 336 | Event: AGIExec 337 | Privilege: agi,all 338 | SubEvent: End 339 | Channel: SIP/999999-00000002 340 | CommandId: 1866797246 341 | Command: SET VARIABLE "XIVO_AGENTEXISTS" "0" 342 | ResultCode: 200 343 | Result: Success 344 | 345 | Event: AGIExec 346 | Privilege: agi,all 347 | SubEvent: Start 348 | Channel: SIP/999999-00000002 349 | CommandId: 928872635 350 | Command: SET VARIABLE "XIVO_AGENTEXISTS" "1" 351 | 352 | Event: VarSet 353 | Privilege: dialplan,all 354 | Channel: SIP/999999-00000002 355 | Variable: XIVO_AGENTEXISTS 356 | Value: 1 357 | Uniqueid: 1409169987.2 358 | 359 | Event: AGIExec 360 | Privilege: agi,all 361 | SubEvent: End 362 | Channel: SIP/999999-00000002 363 | CommandId: 928872635 364 | Command: SET VARIABLE "XIVO_AGENTEXISTS" "1" 365 | ResultCode: 200 366 | Result: Success 367 | 368 | Event: AGIExec 369 | Privilege: agi,all 370 | SubEvent: Start 371 | Channel: SIP/999999-00000002 372 | CommandId: 471755444 373 | Command: SET VARIABLE "XIVO_AGENTPASSWD" "" 374 | 375 | Event: VarSet 376 | Privilege: dialplan,all 377 | Channel: SIP/999999-00000002 378 | Variable: XIVO_AGENTPASSWD 379 | Value: 380 | Uniqueid: 1409169987.2 381 | 382 | Event: AGIExec 383 | Privilege: agi,all 384 | SubEvent: End 385 | Channel: SIP/999999-00000002 386 | CommandId: 471755444 387 | Command: SET VARIABLE "XIVO_AGENTPASSWD" "" 388 | ResultCode: 200 389 | Result: Success 390 | 391 | Event: AGIExec 392 | Privilege: agi,all 393 | SubEvent: Start 394 | Channel: SIP/999999-00000002 395 | CommandId: 943373659 396 | Command: SET VARIABLE "XIVO_AGENTID" "3" 397 | 398 | Event: VarSet 399 | Privilege: dialplan,all 400 | Channel: SIP/999999-00000002 401 | Variable: XIVO_AGENTID 402 | Value: 3 403 | Uniqueid: 1409169987.2 404 | 405 | [1613 bytes missing in capture file]Event: AGIExec 406 | Privilege: agi,all 407 | SubEvent: Start 408 | Channel: SIP/999999-00000002 409 | CommandId: 1220663899 410 | Command: SET VARIABLE "XIVO_AGENT_LOGIN_STATUS" "logged_out" 411 | 412 | Event: VarSet 413 | Privilege: dialplan,all 414 | Channel: SIP/999999-00000002 415 | Variable: XIVO_AGENT_LOGIN_STATUS 416 | Value: logged_out 417 | Uniqueid: 1409169987.2 418 | 419 | Event: AGIExec 420 | Privilege: agi,all 421 | SubEvent: End 422 | Channel: SIP/999999-00000002 423 | CommandId: 1220663899 424 | Command: SET VARIABLE "XIVO_AGENT_LOGIN_STATUS" "logged_out" 425 | ResultCode: 200 426 | Result: Success 427 | 428 | Event: AGIExec 429 | Privilege: agi,all 430 | SubEvent: Start 431 | Channel: SIP/999999-00000002 432 | CommandId: 1760817301 433 | Command: VERBOSE "AGI handler 'agent_get_status' successfully executed" 1 434 | 435 | Event: AGIExec 436 | Privilege: agi,all 437 | SubEvent: End 438 | Channel: SIP/999999-00000002 439 | CommandId: 1760817301 440 | Command: VERBOSE "AGI handler 'agent_get_status' successfully executed" 1 441 | ResultCode: 200 442 | Result: Success 443 | 444 | Event: VarSet 445 | Privilege: dialplan,all 446 | Channel: SIP/999999-00000002 447 | Variable: AGISTATUS 448 | Value: SUCCESS 449 | Uniqueid: 1409169987.2 450 | 451 | Event: Newexten 452 | Privilege: dialplan,all 453 | Channel: SIP/999999-00000002 454 | Context: agentstaticlogtoggle 455 | Extension: s 456 | Priority: 7 457 | Application: Goto 458 | AppData: login_status_logged_out,1 459 | Uniqueid: 1409169987.2 460 | 461 | Event: Newexten 462 | Privilege: dialplan,all 463 | Channel: SIP/999999-00000002 464 | Context: agentstaticlogtoggle 465 | Extension: login_status_logged_out 466 | Priority: 1 467 | Application: NoOp 468 | AppData: 469 | Uniqueid: 1409169987.2 470 | 471 | Event: Newexten 472 | Privilege: dialplan,all 473 | Channel: SIP/999999-00000002 474 | Context: agentstaticlogtoggle 475 | Extension: login_status_logged_out 476 | Priority: 2 477 | Application: Gosub 478 | AppData: agentstaticlogin,s,1(402) 479 | Uniqueid: 1409169987.2 480 | 481 | Event: VarSet 482 | Privilege: dialplan,all 483 | Channel: SIP/999999-00000002 484 | Variable: LOCAL(ARG1) 485 | Value: 402 486 | Uniqueid: 1409169987.2 487 | 488 | Event: VarSet 489 | Privilege: dialplan,all 490 | Channel: SIP/999999-00000002 491 | Variable: LOCAL(ARGC) 492 | Value: 1 493 | Uniqueid: 1409169987.2 494 | 495 | Event: AGIExec 496 | Privilege: agi,all 497 | SubEvent: Start 498 | Channel: SIP/999999-00000002 499 | CommandId: 1403387703 500 | Command: SET VARIABLE "XIVO_AGENTEXISTS" "0" 501 | 502 | Event: VarSet 503 | Privilege: dialplan,all 504 | Channel: SIP/999999-00000002 505 | Variable: XIVO_AGENTEXISTS 506 | Value: 0 507 | Uniqueid: 1409169987.2 508 | 509 | Event: AGIExec 510 | Privilege: agi,all 511 | SubEvent: End 512 | Channel: SIP/999999-00000002 513 | CommandId: 1403387703 514 | Command: SET VARIABLE "XIVO_AGENTEXISTS" "0" 515 | ResultCode: 200 516 | Result: Success 517 | 518 | Event: AGIExec 519 | Privilege: agi,all 520 | SubEvent: Start 521 | Channel: SIP/999999-00000002 522 | CommandId: 1033098297 523 | Command: SET VARIABLE "XIVO_AGENTEXISTS" "1" 524 | 525 | Event: VarSet 526 | Privilege: dialplan,all 527 | Channel: SIP/999999-00000002 528 | Variable: XIVO_AGENTEXISTS 529 | Value: 1 530 | Uniqueid: 1409169987.2 531 | 532 | Event: AGIExec 533 | Privilege: agi,all 534 | SubEvent: End 535 | Channel: SIP/999999-00000002 536 | CommandId: 1033098297 537 | Command: SET VARIABLE "XIVO_AGENTEXISTS" "1" 538 | ResultCode: 200 539 | Result: Success 540 | 541 | Event: AGIExec 542 | Privilege: agi,all 543 | SubEvent: Start 544 | Channel: SIP/999999-00000002 545 | CommandId: 1343196088 546 | Command: SET VARIABLE "XIVO_AGENTPASSWD" "" 547 | 548 | Event: VarSet 549 | Privilege: dialplan,all 550 | Channel: SIP/999999-00000002 551 | Variable: XIVO_AGENTPASSWD 552 | Value: 553 | Uniqueid: 1409169987.2 554 | 555 | Event: AGIExec 556 | Privilege: agi,all 557 | SubEvent: End 558 | Channel: SIP/999999-00000002 559 | CommandId: 1343196088 560 | Command: SET VARIABLE "XIVO_AGENTPASSWD" "" 561 | ResultCode: 200 562 | Result: Success 563 | 564 | Event: AGIExec 565 | Privilege: agi,all 566 | SubEvent: Start 567 | Channel: SIP/999999-00000002 568 | CommandId: 897606607 569 | Command: SET VARIABLE "XIVO_AGENTID" "3" 570 | 571 | Event: VarSet 572 | Privilege: dialplan,all 573 | Channel: SIP/999999-00000002 574 | Variable: XIVO_AGENTID 575 | Value: 3 576 | Uniqueid: 1409169987.2 577 | 578 | Event: AGIExec 579 | Privilege: agi,all 580 | SubEvent: End 581 | Channel: SIP/999999-00000002 582 | CommandId: 897606607 583 | Command: SET VARIABLE "XIVO_AGENTID" "3" 584 | ResultCode: 200 585 | Result: Success 586 | 587 | Event: AGIExec 588 | Privilege: agi,all 589 | SubEvent: Start 590 | Channel: SIP/999999-00000002 591 | CommandId: 830831622 592 | Command: SET VARIABLE "XIVO_AGENTNUM" "402" 593 | 594 | Event: VarSet 595 | Privilege: dialplan,all 596 | Channel: SIP/999999-00000002 597 | Variable: XIVO_AGENTNUM 598 | Value: 402 599 | Uniqueid: 1409169987.2 600 | 601 | Event: AGIExec 602 | Privilege: agi,all 603 | SubEvent: End 604 | Channel: SIP/999999-00000002 605 | CommandId: 830831622 606 | Command: SET VARIABLE "XIVO_AGENTNUM" "402" 607 | ResultCode: 200 608 | Result: Success 609 | 610 | Event: AGIExec 611 | Privilege: agi,all 612 | SubEvent: Start 613 | Channel: SIP/999999-00000002 614 | CommandId: 1329768506 615 | Command: VERBOSE "AGI handler 'agent_get_options' successfully executed" 1 616 | 617 | Event: AGIExec 618 | Privilege: agi,all 619 | SubEvent: End 620 | Channel: SIP/999999-00000002 621 | CommandId: 1329768506 622 | Command: VERBOSE "AGI handler 'agent_get_options' successfully executed" 1 623 | ResultCode: 200 624 | Result: Success 625 | 626 | Event: VarSet 627 | Privilege: dialplan,all 628 | Channel: SIP/999999-00000002 629 | Variable: AGISTATUS 630 | Value: SUCCESS 631 | Uniqueid: 1409169987.2 632 | 633 | Event: Newexten 634 | Privilege: dialplan,all 635 | Channel: SIP/999999-00000002 636 | Context: agentstaticlogin 637 | Extension: s 638 | Priority: 7 639 | Application: GotoIf 640 | AppData: 0?error_no_such_agent,1 641 | Uniqueid: 1409169987.2 642 | 643 | Event: Newexten 644 | Privilege: dialplan,all 645 | Channel: SIP/999999-00000002 646 | Context: agentstaticlogin 647 | Extension: s 648 | Priority: 8 649 | Application: GosubIf 650 | AppData: 0?authenticate,1 651 | Uniqueid: 1409169987.2 652 | 653 | Event: Newexten 654 | Privilege: dialplan,all 655 | Channel: SIP/999999-00000002 656 | Context: agentstaticlogin 657 | Extension: s 658 | Priority: 9 659 | Application: AGI 660 | AppData: agi://127.0.0.1/agent_login,3,202,default 661 | Uniqueid: 1409169987.2 662 | 663 | Action: Ping 664 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/28 665 | 666 | Response: Success 667 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/28 668 | Ping: Pong 669 | Timestamp: 1409169989.456835 670 | 671 | Event: QueueMemberAdded 672 | Privilege: agent,all 673 | Queue: general_queue 674 | Location: Local/id-3@agentcallback 675 | MemberName: Agent/402 676 | StateInterface: SIP/999999 677 | Membership: dynamic 678 | Penalty: 0 679 | CallsTaken: 0 680 | LastCall: 0 681 | Status: 2 682 | Paused: 0 683 | 684 | Event: UserEvent 685 | Privilege: user,all 686 | UserEvent: AgentLogin 687 | Action: UserEvent 688 | ActionID: 2 689 | AgentID: 3 690 | AgentNumber: 402 691 | Extension: 202 692 | Context: default 693 | 694 | Event: AGIExec 695 | Privilege: agi,all 696 | SubEvent: Start 697 | Channel: SIP/999999-00000002 698 | CommandId: 1580723124 699 | Command: SET VARIABLE "XIVO_AGENTSTATUS" "logged" 700 | 701 | Event: VarSet 702 | Privilege: dialplan,all 703 | Channel: SIP/999999-00000002 704 | Variable: XIVO_AGENTSTATUS 705 | Value: logged 706 | Uniqueid: 1409169987.2 707 | 708 | Event: AGIExec 709 | Privilege: agi,all 710 | SubEvent: End 711 | Channel: SIP/999999-00000002 712 | CommandId: 1580723124 713 | Command: SET VARIABLE "XIVO_AGENTSTATUS" "logged" 714 | ResultCode: 200 715 | Result: Success 716 | 717 | Event: AGIExec 718 | Privilege: agi,all 719 | SubEvent: Start 720 | Channel: SIP/999999-00000002 721 | CommandId: 574066156 722 | Command: VERBOSE "AGI handler 'agent_login' successfully executed" 1 723 | 724 | Event: AGIExec 725 | Privilege: agi,all 726 | SubEvent: End 727 | Channel: SIP/999999-00000002 728 | CommandId: 574066156 729 | Command: VERBOSE "AGI handler 'agent_login' successfully executed" 1 730 | ResultCode: 200 731 | Result: Success 732 | 733 | Event: VarSet 734 | Privilege: dialplan,all 735 | Channel: SIP/999999-00000002 736 | Variable: AGISTATUS 737 | Value: SUCCESS 738 | Uniqueid: 1409169987.2 739 | 740 | Event: Newexten 741 | Privilege: dialplan,all 742 | Channel: SIP/999999-00000002 743 | Context: agentstaticlogin 744 | Extension: s 745 | Priority: 10 746 | Application: Goto 747 | AppData: status_logged,1 748 | Uniqueid: 1409169987.2 749 | 750 | Event: Newexten 751 | Privilege: dialplan,all 752 | Channel: SIP/999999-00000002 753 | Context: agentstaticlogin 754 | Extension: status_logged 755 | Priority: 1 756 | Application: NoOp 757 | AppData: 758 | Uniqueid: 1409169987.2 759 | 760 | Event: Newexten 761 | Privilege: dialplan,all 762 | Channel: SIP/999999-00000002 763 | Context: agentstaticlogin 764 | Extension: status_logged 765 | Priority: 2 766 | Application: Playback 767 | AppData: agent-loginok 768 | Uniqueid: 1409169987.2 769 | 770 | Event: RTCPReceived 771 | Privilege: reporting,all 772 | From: 192.168.1.5:60377 773 | PT: 201(Receiver Report) 774 | ReceptionReports: 1 775 | SenderSSRC: 2314535167 776 | FractionLost: 255 777 | PacketsLost: 62857 778 | HighestSequence: 62911 779 | SequenceNumberCycles: 0 780 | IAJitter: 154953728 781 | LastSR: 0.0000000000 782 | DLSR: 0.0000(sec) 783 | 784 | Event: VarSet 785 | Privilege: dialplan,all 786 | Channel: SIP/999999-00000002 787 | Variable: PLAYBACKSTATUS 788 | Value: SUCCESS 789 | Uniqueid: 1409169987.2 790 | 791 | Event: Newexten 792 | Privilege: dialplan,all 793 | Channel: SIP/999999-00000002 794 | Context: agentstaticlogin 795 | Extension: status_logged 796 | Priority: 3 797 | Application: AGI 798 | AppData: agi://127.0.0.1/phone_progfunckey_devstate,agentstaticlogin,INUSE,*3 799 | Uniqueid: 1409169987.2 800 | 801 | Event: AGIExec 802 | Privilege: agi,all 803 | SubEvent: Start 804 | Channel: SIP/999999-00000002 805 | CommandId: 404007469 806 | Command: GET VARIABLE "XIVO_USERID" 807 | 808 | Event: AGIExec 809 | Privilege: agi,all 810 | SubEvent: End 811 | Channel: SIP/999999-00000002 812 | CommandId: 404007469 813 | Command: GET VARIABLE "XIVO_USERID" 814 | ResultCode: 200 815 | Result: Success 816 | 817 | Event: AGIExec 818 | Privilege: agi,all 819 | SubEvent: Start 820 | Channel: SIP/999999-00000002 821 | CommandId: 469538082 822 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***231***33)" "INUSE" 823 | 824 | Event: AGIExec 825 | Privilege: agi,all 826 | SubEvent: End 827 | Channel: SIP/999999-00000002 828 | CommandId: 469538082 829 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***231***33)" "INUSE" 830 | ResultCode: 200 831 | Result: Success 832 | 833 | Event: AGIExec 834 | Privilege: agi,all 835 | SubEvent: Start 836 | Channel: SIP/999999-00000002 837 | CommandId: 1817837906 838 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 839 | 840 | Event: AGIExec 841 | Privilege: agi,all 842 | SubEvent: End 843 | Channel: SIP/999999-00000002 844 | CommandId: 1817837906 845 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 846 | ResultCode: 200 847 | Result: Success 848 | 849 | Event: VarSet 850 | Privilege: dialplan,all 851 | Channel: SIP/999999-00000002 852 | Variable: AGISTATUS 853 | Value: SUCCESS 854 | Uniqueid: 1409169987.2 855 | 856 | Event: Newexten 857 | Privilege: dialplan,all 858 | Channel: SIP/999999-00000002 859 | Context: agentstaticlogin 860 | Extension: status_logged 861 | Priority: 4 862 | Application: AGI 863 | AppData: agi://127.0.0.1/phone_progfunckey_devstate,agentstaticlogin,INUSE,402 864 | Uniqueid: 1409169987.2 865 | 866 | Event: AGIExec 867 | Privilege: agi,all 868 | SubEvent: Start 869 | Channel: SIP/999999-00000002 870 | CommandId: 1638740008 871 | Command: GET VARIABLE "XIVO_USERID" 872 | 873 | Event: AGIExec 874 | Privilege: agi,all 875 | SubEvent: End 876 | Channel: SIP/999999-00000002 877 | CommandId: 1638740008 878 | Command: GET VARIABLE "XIVO_USERID" 879 | ResultCode: 200 880 | Result: Success 881 | 882 | Event: AGIExec 883 | Privilege: agi,all 884 | SubEvent: Start 885 | Channel: SIP/999999-00000002 886 | CommandId: 513441949 887 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***231*402)" "INUSE" 888 | 889 | Event: AGIExec 890 | Privilege: agi,all 891 | SubEvent: End 892 | Channel: SIP/999999-00000002 893 | CommandId: 513441949 894 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***231*402)" "INUSE" 895 | ResultCode: 200 896 | Result: Success 897 | 898 | Event: AGIExec 899 | Privilege: agi,all 900 | SubEvent: Start 901 | Channel: SIP/999999-00000002 902 | CommandId: 1638230248 903 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 904 | 905 | Event: AGIExec 906 | Privilege: agi,all 907 | SubEvent: End 908 | Channel: SIP/999999-00000002 909 | CommandId: 1638230248 910 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 911 | ResultCode: 200 912 | Result: Success 913 | 914 | Event: VarSet 915 | Privilege: dialplan,all 916 | Channel: SIP/999999-00000002 917 | Variable: AGISTATUS 918 | Value: SUCCESS 919 | Uniqueid: 1409169987.2 920 | 921 | Event: Newexten 922 | Privilege: dialplan,all 923 | Channel: SIP/999999-00000002 924 | Context: agentstaticlogin 925 | Extension: status_logged 926 | Priority: 5 927 | Application: AGI 928 | AppData: agi://127.0.0.1/phone_progfunckey_devstate,agentstaticlogoff,NOT_INUSE,*3 929 | Uniqueid: 1409169987.2 930 | 931 | Event: AGIExec 932 | Privilege: agi,all 933 | SubEvent: Start 934 | Channel: SIP/999999-00000002 935 | CommandId: 2095413575 936 | Command: GET VARIABLE "XIVO_USERID" 937 | 938 | Event: AGIExec 939 | Privilege: agi,all 940 | SubEvent: End 941 | Channel: SIP/999999-00000002 942 | CommandId: 2095413575 943 | Command: GET VARIABLE "XIVO_USERID" 944 | ResultCode: 200 945 | Result: Success 946 | 947 | Event: AGIExec 948 | Privilege: agi,all 949 | SubEvent: Start 950 | Channel: SIP/999999-00000002 951 | CommandId: 940331895 952 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***232***33)" "NOT_INUSE" 953 | 954 | Event: AGIExec 955 | Privilege: agi,all 956 | SubEvent: End 957 | Channel: SIP/999999-00000002 958 | CommandId: 940331895 959 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***232***33)" "NOT_INUSE" 960 | ResultCode: 200 961 | Result: Success 962 | 963 | Event: AGIExec 964 | Privilege: agi,all 965 | SubEvent: Start 966 | Channel: SIP/999999-00000002 967 | CommandId: 1759701864 968 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 969 | 970 | Event: AGIExec 971 | Privilege: agi,all 972 | SubEvent: End 973 | Channel: SIP/999999-00000002 974 | CommandId: 1759701864 975 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 976 | ResultCode: 200 977 | Result: Success 978 | 979 | Event: VarSet 980 | Privilege: dialplan,all 981 | Channel: SIP/999999-00000002 982 | Variable: AGISTATUS 983 | Value: SUCCESS 984 | Uniqueid: 1409169987.2 985 | 986 | Event: Newexten 987 | Privilege: dialplan,all 988 | Channel: SIP/999999-00000002 989 | Context: agentstaticlogin 990 | Extension: status_logged 991 | Priority: 6 992 | Application: AGI 993 | AppData: agi://127.0.0.1/phone_progfunckey_devstate,agentstaticlogoff,NOT_INUSE,402 994 | Uniqueid: 1409169987.2 995 | 996 | Event: AGIExec 997 | Privilege: agi,all 998 | SubEvent: Start 999 | Channel: SIP/999999-00000002 1000 | CommandId: 268900170 1001 | Command: GET VARIABLE "XIVO_USERID" 1002 | 1003 | Event: AGIExec 1004 | Privilege: agi,all 1005 | SubEvent: End 1006 | Channel: SIP/999999-00000002 1007 | CommandId: 268900170 1008 | Command: GET VARIABLE "XIVO_USERID" 1009 | ResultCode: 200 1010 | Result: Success 1011 | 1012 | Event: AGIExec 1013 | Privilege: agi,all 1014 | SubEvent: Start 1015 | Channel: SIP/999999-00000002 1016 | CommandId: 364145573 1017 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***232*402)" "NOT_INUSE" 1018 | 1019 | Event: AGIExec 1020 | Privilege: agi,all 1021 | SubEvent: End 1022 | Channel: SIP/999999-00000002 1023 | CommandId: 364145573 1024 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***232*402)" "NOT_INUSE" 1025 | ResultCode: 200 1026 | Result: Success 1027 | 1028 | Event: AGIExec 1029 | Privilege: agi,all 1030 | SubEvent: Start 1031 | Channel: SIP/999999-00000002 1032 | CommandId: 449661018 1033 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 1034 | 1035 | Event: AGIExec 1036 | Privilege: agi,all 1037 | SubEvent: End 1038 | Channel: SIP/999999-00000002 1039 | CommandId: 449661018 1040 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 1041 | ResultCode: 200 1042 | Result: Success 1043 | 1044 | Event: VarSet 1045 | Privilege: dialplan,all 1046 | Channel: SIP/999999-00000002 1047 | Variable: AGISTATUS 1048 | Value: SUCCESS 1049 | Uniqueid: 1409169987.2 1050 | 1051 | Event: Newexten 1052 | Privilege: dialplan,all 1053 | Channel: SIP/999999-00000002 1054 | Context: agentstaticlogin 1055 | Extension: status_logged 1056 | Priority: 7 1057 | Application: AGI 1058 | AppData: agi://127.0.0.1/phone_progfunckey_devstate,agentstaticlogtoggle,INUSE,*3 1059 | Uniqueid: 1409169987.2 1060 | 1061 | Event: AGIExec 1062 | Privilege: agi,all 1063 | SubEvent: Start 1064 | Channel: SIP/999999-00000002 1065 | CommandId: 1650004488 1066 | Command: GET VARIABLE "XIVO_USERID" 1067 | 1068 | Event: AGIExec 1069 | Privilege: agi,all 1070 | SubEvent: End 1071 | Channel: SIP/999999-00000002 1072 | CommandId: 1650004488 1073 | Command: GET VARIABLE "XIVO_USERID" 1074 | ResultCode: 200 1075 | Result: Success 1076 | 1077 | Event: AGIExec 1078 | Privilege: agi,all 1079 | SubEvent: Start 1080 | Channel: SIP/999999-00000002 1081 | CommandId: 1560610405 1082 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***230***33)" "INUSE" 1083 | 1084 | Event: AGIExec 1085 | Privilege: agi,all 1086 | SubEvent: End 1087 | Channel: SIP/999999-00000002 1088 | CommandId: 1560610405 1089 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***230***33)" "INUSE" 1090 | ResultCode: 200 1091 | Result: Success 1092 | 1093 | Event: AGIExec 1094 | Privilege: agi,all 1095 | SubEvent: Start 1096 | Channel: SIP/999999-00000002 1097 | CommandId: 1767493288 1098 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 1099 | 1100 | Event: AGIExec 1101 | Privilege: agi,all 1102 | SubEvent: End 1103 | Channel: SIP/999999-00000002 1104 | CommandId: 1767493288 1105 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 1106 | ResultCode: 200 1107 | Result: Success 1108 | 1109 | Event: VarSet 1110 | Privilege: dialplan,all 1111 | Channel: SIP/999999-00000002 1112 | Variable: AGISTATUS 1113 | Value: SUCCESS 1114 | Uniqueid: 1409169987.2 1115 | 1116 | Event: Newexten 1117 | Privilege: dialplan,all 1118 | Channel: SIP/999999-00000002 1119 | Context: agentstaticlogin 1120 | Extension: status_logged 1121 | Priority: 8 1122 | Application: AGI 1123 | AppData: agi://127.0.0.1/phone_progfunckey_devstate,agentstaticlogtoggle,INUSE,402 1124 | Uniqueid: 1409169987.2 1125 | 1126 | Event: AGIExec 1127 | Privilege: agi,all 1128 | SubEvent: Start 1129 | Channel: SIP/999999-00000002 1130 | CommandId: 850973468 1131 | Command: GET VARIABLE "XIVO_USERID" 1132 | 1133 | Event: AGIExec 1134 | Privilege: agi,all 1135 | SubEvent: End 1136 | Channel: SIP/999999-00000002 1137 | CommandId: 850973468 1138 | Command: GET VARIABLE "XIVO_USERID" 1139 | ResultCode: 200 1140 | Result: Success 1141 | 1142 | Event: AGIExec 1143 | Privilege: agi,all 1144 | SubEvent: Start 1145 | Channel: SIP/999999-00000002 1146 | CommandId: 1978695696 1147 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***230*402)" "INUSE" 1148 | 1149 | Event: AGIExec 1150 | Privilege: agi,all 1151 | SubEvent: End 1152 | Channel: SIP/999999-00000002 1153 | CommandId: 1978695696 1154 | Command: SET VARIABLE "DEVICE_STATE(Custom:*7352***230*402)" "INUSE" 1155 | ResultCode: 200 1156 | Result: Success 1157 | 1158 | Event: AGIExec 1159 | Privilege: agi,all 1160 | SubEvent: Start 1161 | Channel: SIP/999999-00000002 1162 | CommandId: 1831807943 1163 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 1164 | 1165 | Event: AGIExec 1166 | Privilege: agi,all 1167 | SubEvent: End 1168 | Channel: SIP/999999-00000002 1169 | CommandId: 1831807943 1170 | Command: VERBOSE "AGI handler 'phone_progfunckey_devstate' successfully executed" 1 1171 | ResultCode: 200 1172 | Result: Success 1173 | 1174 | Event: VarSet 1175 | Privilege: dialplan,all 1176 | Channel: SIP/999999-00000002 1177 | Variable: AGISTATUS 1178 | Value: SUCCESS 1179 | Uniqueid: 1409169987.2 1180 | 1181 | Event: Newexten 1182 | Privilege: dialplan,all 1183 | Channel: SIP/999999-00000002 1184 | Context: agentstaticlogin 1185 | Extension: status_logged 1186 | Priority: 9 1187 | Application: Hangup 1188 | AppData: 1189 | Uniqueid: 1409169987.2 1190 | 1191 | Event: SoftHangupRequest 1192 | Privilege: call,all 1193 | Channel: SIP/999999-00000002 1194 | Uniqueid: 1409169987.2 1195 | Cause: 16 1196 | 1197 | Event: VarSet 1198 | Privilege: dialplan,all 1199 | Channel: SIP/999999-00000002 1200 | Variable: RTPAUDIOQOS 1201 | Value: ssrc=2057792214;themssrc=1743037615;lp=0;rxjitter=2364.000000;rxcount=229;txjitter=0.019554;txcount=68;rlp=62857;rtt=0.000000 1202 | Uniqueid: 1409169987.2 1203 | 1204 | Event: VarSet 1205 | Privilege: dialplan,all 1206 | Channel: SIP/999999-00000002 1207 | Variable: RTPAUDIOQOSJITTER 1208 | Value: minrxjitter=0.000000;maxrxjitter=0.000000;avgrxjitter=0.000000;stdevrxjitter=0.000000;reported_minjitter=0.000000;reported_maxjitter=0.000000;reported_avgjitter=0.000000;reported_stdevjitter=0.000000; 1209 | Uniqueid: 1409169987.2 1210 | 1211 | Event: VarSet 1212 | Privilege: dialplan,all 1213 | Channel: SIP/999999-00000002 1214 | Variable: RTPAUDIOQOSLOSS 1215 | Value: minrxlost=0.000000;maxrxlost=0.000000;avgrxlost=0.000000;stdevrxlost=0.000000;reported_minlost=0.000000;reported_maxlost=0.000000;reported_avglost=0.000000;reported_stdevlost=0.000000; 1216 | Uniqueid: 1409169987.2 1217 | 1218 | Event: VarSet 1219 | Privilege: dialplan,all 1220 | Channel: SIP/999999-00000002 1221 | Variable: RTPAUDIOQOSRTT 1222 | Value: minrtt=0.000000;maxrtt=0.000000;avgrtt=0.000000;stdevrtt=0.000000; 1223 | Uniqueid: 1409169987.2 1224 | 1225 | Event: VarSet 1226 | Privilege: dialplan,all 1227 | Channel: SIP/999999-00000002 1228 | Variable: RTPAUDIOQOS 1229 | Value: ssrc=2057792214;themssrc=1743037615;lp=0;rxjitter=2364.000000;rxcount=229;txjitter=0.019554;txcount=68;rlp=62857;rtt=0.000000 1230 | Uniqueid: 1409169987.2 1231 | 1232 | Event: CEL 1233 | Privilege: call,all 1234 | EventName: HANGUP 1235 | AccountCode: 1236 | CallerIDnum: 202 1237 | CallerIDname: user 202 1238 | CallerIDani: 202 1239 | CallerIDrdnis: 1240 | CallerIDdnid: *30402 1241 | Exten: status_logged 1242 | Context: agentstaticlogin 1243 | Channel: SIP/999999-00000002 1244 | Application: 1245 | AppData: 1246 | EventTime: 2014-08-27 21:06:32 1247 | AMAFlags: DOCUMENTATION 1248 | UniqueID: 1409169987.2 1249 | LinkedID: 1409169987.2 1250 | Userfield: 1251 | Peer: 1252 | PeerAccount: 1253 | Extra: 16,dialplan/builtin, 1254 | 1255 | Event: QueueMemberStatus 1256 | Privilege: agent,all 1257 | Queue: general_queue 1258 | Location: Local/id-3@agentcallback 1259 | MemberName: Agent/402 1260 | StateInterface: SIP/999999 1261 | Membership: dynamic 1262 | Penalty: 0 1263 | CallsTaken: 0 1264 | LastCall: 0 1265 | Status: 1 1266 | Paused: 0 1267 | Skills: agent-3 1268 | 1269 | Event: ExtensionStatus 1270 | Privilege: call,all 1271 | Exten: 202 1272 | Context: default 1273 | Hint: SIP/999999 1274 | Status: 0 1275 | 1276 | Event: ExtensionStatus 1277 | Privilege: call,all 1278 | Exten: *6662 1279 | Context: default 1280 | Hint: SIP/999999 1281 | Status: 0 1282 | 1283 | Event: CEL 1284 | Privilege: call,all 1285 | EventName: CHAN_END 1286 | AccountCode: 1287 | CallerIDnum: 202 1288 | CallerIDname: user 202 1289 | CallerIDani: 202 1290 | CallerIDrdnis: 1291 | CallerIDdnid: *30402 1292 | Exten: status_logged 1293 | Context: agentstaticlogin 1294 | Channel: SIP/999999-00000002 1295 | Application: 1296 | AppData: 1297 | EventTime: 2014-08-27 21:06:32 1298 | AMAFlags: DOCUMENTATION 1299 | UniqueID: 1409169987.2 1300 | LinkedID: 1409169987.2 1301 | Userfield: 1302 | Peer: 1303 | PeerAccount: 1304 | Extra: 1305 | 1306 | Event: CEL 1307 | Privilege: call,all 1308 | EventName: LINKEDID_END 1309 | AccountCode: 1310 | CallerIDnum: 202 1311 | CallerIDname: user 202 1312 | CallerIDani: 202 1313 | CallerIDrdnis: 1314 | CallerIDdnid: *30402 1315 | Exten: status_logged 1316 | Context: agentstaticlogin 1317 | Channel: SIP/999999-00000002 1318 | Application: 1319 | AppData: 1320 | EventTime: 2014-08-27 21:06:32 1321 | AMAFlags: DOCUMENTATION 1322 | UniqueID: 1409169987.2 1323 | LinkedID: 1409169987.2 1324 | Userfield: 1325 | Peer: 1326 | PeerAccount: 1327 | Extra: 1328 | 1329 | Event: QueueMemberStatus 1330 | Privilege: agent,all 1331 | Queue: general_queue 1332 | Location: Local/id-3@agentcallback 1333 | MemberName: Agent/402 1334 | StateInterface: SIP/999999 1335 | Membership: dynamic 1336 | Penalty: 0 1337 | CallsTaken: 0 1338 | LastCall: 0 1339 | Status: 1 1340 | Paused: 0 1341 | Skills: agent-3 1342 | 1343 | Action: Ping 1344 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/29 1345 | 1346 | Response: Success 1347 | ActionID: b4856f7a-e61e-483b-84e6-12e7d26ea73d/1/29 1348 | Ping: Pong 1349 | Timestamp: 1409169999.470280 1350 | 1351 | -------------------------------------------------------------------------------- /tests/fixtures/agent_not_in_pause.yaml: -------------------------------------------------------------------------------- 1 | Event: Newchannel 2 | Privilege: call,all 3 | Channel: SIP/00000-0000095f 4 | ChannelState: 0 5 | ChannelStateDesc: Down 6 | CallerIDNum: 2001 7 | CallerIDName: Ludovic Gasc 8 | AccountCode: 9 | Exten: *12 10 | Context: default 11 | Uniqueid: 1413823936.3729 12 | 13 | Event: VarSet 14 | Privilege: dialplan,all 15 | Channel: SIP/00000-0000095f 16 | Variable: SIPURI 17 | Value: sip:00000@192.168.2.27:5060 18 | Uniqueid: 1413823936.3729 19 | 20 | Event: VarSet 21 | Privilege: dialplan,all 22 | Channel: SIP/00000-0000095f 23 | Variable: SIPDOMAIN 24 | Value: yyyyyyyyyy.xxx 25 | Uniqueid: 1413823936.3729 26 | 27 | Event: VarSet 28 | Privilege: dialplan,all 29 | Channel: SIP/00000-0000095f 30 | Variable: SIPCALLID 31 | Value: baecd088-e735-4b1e-8b13-fbc88b3d90ef 32 | Uniqueid: 1413823936.3729 33 | 34 | Event: VarSet 35 | Privilege: dialplan,all 36 | Channel: SIP/00000-0000095f 37 | Variable: XIVO_USERID 38 | Value: 2 39 | Uniqueid: 1413823936.3729 40 | 41 | Event: VarSet 42 | Privilege: dialplan,all 43 | Channel: SIP/00000-0000095f 44 | Variable: PICKUPMARK 45 | Value: 2001%default 46 | Uniqueid: 1413823936.3729 47 | 48 | Event: VarSet 49 | Privilege: dialplan,all 50 | Channel: SIP/00000-0000095f 51 | Variable: TRANSFER_CONTEXT 52 | Value: default 53 | Uniqueid: 1413823936.3729 54 | 55 | Event: ChannelUpdate 56 | Privilege: system,all 57 | Channel: SIP/00000-0000095f 58 | Uniqueid: 1413823936.3729 59 | Channeltype: SIP 60 | SIPcallid: baecd088-e735-4b1e-8b13-fbc88b3d90ef 61 | SIPfullcontact: sip:00000@83.101.5.124:5060 62 | 63 | Event: Newstate 64 | Privilege: call,all 65 | Channel: SIP/00000-0000095f 66 | ChannelState: 4 67 | ChannelStateDesc: Ring 68 | CallerIDNum: 2001 69 | CallerIDName: Ludovic Gasc 70 | ConnectedLineNum: 71 | ConnectedLineName: 72 | Uniqueid: 1413823936.3729 73 | 74 | Event: CEL 75 | Privilege: call,all 76 | EventName: CHAN_START 77 | AccountCode: 78 | CallerIDnum: 2001 79 | CallerIDname: Ludovic Gasc 80 | CallerIDani: 81 | CallerIDrdnis: 82 | CallerIDdnid: 83 | Exten: *12 84 | Context: default 85 | Channel: SIP/00000-0000095f 86 | Application: 87 | AppData: 88 | EventTime: 2014-10-20 18:52:16 89 | AMAFlags: DOCUMENTATION 90 | UniqueID: 1413823936.3729 91 | LinkedID: 1413823936.3729 92 | Userfield: 93 | Peer: 94 | PeerAccount: 95 | Extra: 96 | 97 | Event: Newexten 98 | Privilege: dialplan,all 99 | Channel: SIP/00000-0000095f 100 | Context: default 101 | Extension: *12 102 | Priority: 1 103 | Application: Answer 104 | AppData: 105 | Uniqueid: 1413823936.3729 106 | 107 | Event: Newstate 108 | Privilege: call,all 109 | Channel: SIP/00000-0000095f 110 | ChannelState: 6 111 | ChannelStateDesc: Up 112 | CallerIDNum: 2001 113 | CallerIDName: Ludovic Gasc 114 | ConnectedLineNum: 115 | ConnectedLineName: 116 | Uniqueid: 1413823936.3729 117 | 118 | Event: CEL 119 | Privilege: call,all 120 | EventName: ANSWER 121 | AccountCode: 122 | CallerIDnum: 2001 123 | CallerIDname: Ludovic Gasc 124 | CallerIDani: 2001 125 | CallerIDrdnis: 126 | CallerIDdnid: *12 127 | Exten: *12 128 | Context: default 129 | Channel: SIP/00000-0000095f 130 | Application: Answer 131 | AppData: 132 | EventTime: 2014-10-20 18:52:16 133 | AMAFlags: DOCUMENTATION 134 | UniqueID: 1413823936.3729 135 | LinkedID: 1413823936.3729 136 | Userfield: 137 | Peer: 138 | PeerAccount: 139 | Extra: 140 | 141 | Event: ExtensionStatus 142 | Privilege: call,all 143 | Exten: 2001 144 | Context: default 145 | Hint: SIP/00000 146 | Status: 1 147 | 148 | Event: ExtensionStatus 149 | Privilege: call,all 150 | Exten: *6662 151 | Context: default 152 | Hint: SIP/00000 153 | Status: 1 154 | 155 | Event: QueueMemberStatus 156 | Privilege: agent,all 157 | Queue: maisonvoyance-tous 158 | Location: Local/id-12@agentcallback 159 | MemberName: Agent/999 160 | StateInterface: SIP/00000 161 | Membership: dynamic 162 | Penalty: 0 163 | CallsTaken: 0 164 | LastCall: 0 165 | Status: 2 166 | Paused: 1 167 | Skills: agent-12 168 | 169 | Event: QueueMemberStatus 170 | Privilege: agent,all 171 | Queue: maisonvoyance-tous 172 | Location: Local/id-12@agentcallback 173 | MemberName: Agent/999 174 | StateInterface: SIP/00000 175 | Membership: dynamic 176 | Penalty: 0 177 | CallsTaken: 0 178 | LastCall: 0 179 | Status: 2 180 | Paused: 1 181 | Skills: agent-12 182 | 183 | Event: QueueMemberStatus 184 | Privilege: agent,all 185 | Queue: maisonvoyance-tous 186 | Location: Local/id-12@agentcallback 187 | MemberName: Agent/999 188 | StateInterface: SIP/00000 189 | Membership: dynamic 190 | Penalty: 0 191 | CallsTaken: 0 192 | LastCall: 0 193 | Status: 2 194 | Paused: 1 195 | Skills: agent-12 196 | 197 | Event: Newexten 198 | Privilege: dialplan,all 199 | Channel: SIP/00000-0000095f 200 | Context: default 201 | Extension: *12 202 | Priority: 2 203 | Application: DumpChan 204 | AppData: 7 205 | Uniqueid: 1413823936.3729 206 | 207 | Event: Newexten 208 | Privilege: dialplan,all 209 | Channel: SIP/00000-0000095f 210 | Context: default 211 | Extension: *12 212 | Priority: 3 213 | Application: Verbose 214 | AppData: 3, "-= Pause/Unpause agent =-" 215 | Uniqueid: 1413823936.3729 216 | 217 | Event: Newexten 218 | Privilege: dialplan,all 219 | Channel: SIP/00000-0000095f 220 | Context: default 221 | Extension: *12 222 | Priority: 4 223 | Application: Verbose 224 | AppData: 3, "-= Peer name : 00000 =-" 225 | Uniqueid: 1413823936.3729 226 | 227 | Event: Newexten 228 | Privilege: dialplan,all 229 | Channel: SIP/00000-0000095f 230 | Context: default 231 | Extension: *12 232 | Priority: 5 233 | Application: Set 234 | AppData: EYEPEA_PEERNAME=SIP/00000 235 | Uniqueid: 1413823936.3729 236 | 237 | Event: VarSet 238 | Privilege: dialplan,all 239 | Channel: SIP/00000-0000095f 240 | Variable: EYEPEA_PEERNAME 241 | Value: SIP/00000 242 | Uniqueid: 1413823936.3729 243 | 244 | Event: Newexten 245 | Privilege: dialplan,all 246 | Channel: SIP/00000-0000095f 247 | Context: default 248 | Extension: *12 249 | Priority: 6 250 | Application: AGI 251 | AppData: agi://127.0.0.1/get_agent_from_peer 252 | Uniqueid: 1413823936.3729 253 | 254 | Event: AGIExec 255 | Privilege: agi,all 256 | SubEvent: Start 257 | Channel: SIP/00000-0000095f 258 | CommandId: 849605471 259 | Command: GET VARIABLE "EYEPEA_PEERNAME" 260 | 261 | Event: AGIExec 262 | Privilege: agi,all 263 | SubEvent: End 264 | Channel: SIP/00000-0000095f 265 | CommandId: 849605471 266 | Command: GET VARIABLE "EYEPEA_PEERNAME" 267 | ResultCode: 200 268 | Result: Success 269 | 270 | Event: AGIExec 271 | Privilege: agi,all 272 | SubEvent: Start 273 | Channel: SIP/00000-0000095f 274 | CommandId: 435836313 275 | Command: SET VARIABLE "DEBUG EYEPEA_AGENT" "[u'Local/id-12@agentcallback']" 276 | 277 | Event: VarSet 278 | Privilege: dialplan,all 279 | Channel: SIP/00000-0000095f 280 | Variable: DEBUG EYEPEA_AGENT 281 | Value: [u'Local/id-12@agentcallback'] 282 | Uniqueid: 1413823936.3729 283 | 284 | Event: AGIExec 285 | Privilege: agi,all 286 | SubEvent: End 287 | Channel: SIP/00000-0000095f 288 | CommandId: 435836313 289 | Command: SET VARIABLE "DEBUG EYEPEA_AGENT" "[u'Local/id-12@agentcallback']" 290 | ResultCode: 200 291 | Result: Success 292 | 293 | Event: AGIExec 294 | Privilege: agi,all 295 | SubEvent: Start 296 | Channel: SIP/00000-0000095f 297 | CommandId: 750822880 298 | Command: SET VARIABLE "EYEPEA_AGENT_INTERFACE" "Local/id-12@agentcallback" 299 | 300 | Event: VarSet 301 | Privilege: dialplan,all 302 | Channel: SIP/00000-0000095f 303 | Variable: EYEPEA_AGENT_INTERFACE 304 | Value: Local/id-12@agentcallback 305 | Uniqueid: 1413823936.3729 306 | 307 | Event: AGIExec 308 | Privilege: agi,all 309 | SubEvent: End 310 | Channel: SIP/00000-0000095f 311 | CommandId: 750822880 312 | Command: SET VARIABLE "EYEPEA_AGENT_INTERFACE" "Local/id-12@agentcallback" 313 | ResultCode: 200 314 | Result: Success 315 | 316 | Event: AGIExec 317 | Privilege: agi,all 318 | SubEvent: Start 319 | Channel: SIP/00000-0000095f 320 | CommandId: 364341601 321 | Command: VERBOSE "AGI handler 'get_agent_from_peer' successfully executed" 1 322 | 323 | Event: AGIExec 324 | Privilege: agi,all 325 | SubEvent: End 326 | Channel: SIP/00000-0000095f 327 | CommandId: 364341601 328 | Command: VERBOSE "AGI handler 'get_agent_from_peer' successfully executed" 1 329 | ResultCode: 200 330 | Result: Success 331 | 332 | Event: VarSet 333 | Privilege: dialplan,all 334 | Channel: SIP/00000-0000095f 335 | Variable: AGISTATUS 336 | Value: SUCCESS 337 | Uniqueid: 1413823936.3729 338 | 339 | Event: Newexten 340 | Privilege: dialplan,all 341 | Channel: SIP/00000-0000095f 342 | Context: default 343 | Extension: *12 344 | Priority: 7 345 | Application: GotoIf 346 | AppData: 0?error 347 | Uniqueid: 1413823936.3729 348 | 349 | Event: Newexten 350 | Privilege: dialplan,all 351 | Channel: SIP/00000-0000095f 352 | Context: default 353 | Extension: *12 354 | Priority: 8 355 | Application: Verbose 356 | AppData: 3, "-= Peer name 00000 uses interface Local/id-12@agentcallback 357 | Uniqueid: 1413823936.3729 358 | 359 | Event: VarSet 360 | Privilege: dialplan,all 361 | Channel: SIP/00000-0000095f 362 | Variable: DB_RESULT 363 | Value: true 364 | Uniqueid: 1413823936.3729 365 | 366 | Event: Newexten 367 | Privilege: dialplan,all 368 | Channel: SIP/00000-0000095f 369 | Context: default 370 | Extension: *12 371 | Priority: 9 372 | Application: GotoIf 373 | AppData: 1?unpause:pause 374 | Uniqueid: 1413823936.3729 375 | 376 | Event: Newexten 377 | Privilege: dialplan,all 378 | Channel: SIP/00000-0000095f 379 | Context: default 380 | Extension: *12 381 | Priority: 14 382 | Application: Verbose 383 | AppData: 3, "-= Unpausing agent Local/id-12@agentcallback =-" 384 | Uniqueid: 1413823936.3729 385 | 386 | Event: Newexten 387 | Privilege: dialplan,all 388 | Channel: SIP/00000-0000095f 389 | Context: default 390 | Extension: *12 391 | Priority: 15 392 | Application: UnpauseQueueMember 393 | AppData: ,Local/id-12@agentcallback 394 | Uniqueid: 1413823936.3729 395 | 396 | Event: QueueMemberPaused 397 | Privilege: agent,all 398 | Queue: maisonvoyance-tous 399 | Location: Local/id-12@agentcallback 400 | MemberName: Agent/999 401 | Paused: 0 402 | 403 | Event: VarSet 404 | Privilege: dialplan,all 405 | Channel: SIP/00000-0000095f 406 | Variable: UPQMSTATUS 407 | Value: UNPAUSED 408 | Uniqueid: 1413823936.3729 409 | 410 | Event: Newexten 411 | Privilege: dialplan,all 412 | Channel: SIP/00000-0000095f 413 | Context: default 414 | Extension: *12 415 | Priority: 16 416 | Application: Set 417 | AppData: DB(pause_active/00000)=false 418 | Uniqueid: 1413823936.3729 419 | 420 | Event: Newexten 421 | Privilege: dialplan,all 422 | Channel: SIP/00000-0000095f 423 | Context: default 424 | Extension: *12 425 | Priority: 17 426 | Application: Goto 427 | AppData: hangup 428 | Uniqueid: 1413823936.3729 429 | 430 | Event: Newexten 431 | Privilege: dialplan,all 432 | Channel: SIP/00000-0000095f 433 | Context: default 434 | Extension: *12 435 | Priority: 19 436 | Application: Hangup 437 | AppData: 16 438 | Uniqueid: 1413823936.3729 439 | 440 | Event: SoftHangupRequest 441 | Privilege: call,all 442 | Channel: SIP/00000-0000095f 443 | Uniqueid: 1413823936.3729 444 | Cause: 16 445 | 446 | Event: VarSet 447 | Privilege: dialplan,all 448 | Channel: SIP/00000-0000095f 449 | Variable: RTPAUDIOQOS 450 | Value: ssrc=143534956;themssrc=0;lp=0;rxjitter=0.000000;rxcount=0;txjitter=0.000000;txcount=0;rlp=0;rtt=0.000000 451 | Uniqueid: 1413823936.3729 452 | 453 | Event: VarSet 454 | Privilege: dialplan,all 455 | Channel: SIP/00000-0000095f 456 | Variable: RTPAUDIOQOSJITTER 457 | Value: minrxjitter=0.000000;maxrxjitter=0.000000;avgrxjitter=0.000000;stdevrxjitter=0.000000;reported_minjitter=0.000000;reported_maxjitter=0.000000;reported_avgjitter=0.000000;reported_stdevjitter=0.000000; 458 | Uniqueid: 1413823936.3729 459 | 460 | Event: VarSet 461 | Privilege: dialplan,all 462 | Channel: SIP/00000-0000095f 463 | Variable: RTPAUDIOQOSLOSS 464 | Value: minrxlost=0.000000;maxrxlost=0.000000;avgrxlost=0.000000;stdevrxlost=0.000000;reported_minlost=0.000000;reported_maxlost=0.000000;reported_avglost=0.000000;reported_stdevlost=0.000000; 465 | Uniqueid: 1413823936.3729 466 | 467 | Event: VarSet 468 | Privilege: dialplan,all 469 | Channel: SIP/00000-0000095f 470 | Variable: RTPAUDIOQOSRTT 471 | Value: minrtt=0.000000;maxrtt=0.000000;avgrtt=0.000000;stdevrtt=0.000000; 472 | Uniqueid: 1413823936.3729 473 | 474 | Event: VarSet 475 | Privilege: dialplan,all 476 | Channel: SIP/00000-0000095f 477 | Variable: RTPAUDIOQOS 478 | Value: ssrc=143534956;themssrc=0;lp=0;rxjitter=0.000000;rxcount=0;txjitter=0.000000;txcount=0;rlp=0;rtt=0.000000 479 | Uniqueid: 1413823936.3729 480 | 481 | Event: Hangup 482 | Privilege: call,all 483 | Channel: SIP/00000-0000095f 484 | Uniqueid: 1413823936.3729 485 | CallerIDNum: 2001 486 | CallerIDName: Ludovic Gasc 487 | ConnectedLineNum: 488 | ConnectedLineName: 489 | AccountCode: 490 | Cause: 16 491 | Cause-txt: Normal Clearing 492 | 493 | Event: CEL 494 | Privilege: call,all 495 | EventName: HANGUP 496 | AccountCode: 497 | CallerIDnum: 2001 498 | CallerIDname: Ludovic Gasc 499 | CallerIDani: 2001 500 | CallerIDrdnis: 501 | CallerIDdnid: *12 502 | Exten: *12 503 | Context: default 504 | Channel: SIP/00000-0000095f 505 | Application: 506 | AppData: 507 | EventTime: 2014-10-20 18:52:16 508 | AMAFlags: DOCUMENTATION 509 | UniqueID: 1413823936.3729 510 | LinkedID: 1413823936.3729 511 | Userfield: 512 | Peer: 513 | PeerAccount: 514 | Extra: 16,dialplan/builtin, 515 | 516 | Event: CEL 517 | Privilege: call,all 518 | EventName: CHAN_END 519 | AccountCode: 520 | CallerIDnum: 2001 521 | CallerIDname: Ludovic Gasc 522 | CallerIDani: 2001 523 | CallerIDrdnis: 524 | CallerIDdnid: *12 525 | Exten: *12 526 | Context: default 527 | Channel: SIP/00000-0000095f 528 | Application: 529 | AppData: 530 | EventTime: 2014-10-20 18:52:16 531 | AMAFlags: DOCUMENTATION 532 | UniqueID: 1413823936.3729 533 | LinkedID: 1413823936.3729 534 | Userfield: 535 | Peer: 536 | PeerAccount: 537 | Extra: 538 | 539 | Event: CEL 540 | Privilege: call,all 541 | EventName: LINKEDID_END 542 | AccountCode: 543 | CallerIDnum: 2001 544 | CallerIDname: Ludovic Gasc 545 | CallerIDani: 2001 546 | CallerIDrdnis: 547 | CallerIDdnid: *12 548 | Exten: *12 549 | Context: default 550 | Channel: SIP/00000-0000095f 551 | Application: 552 | AppData: 553 | EventTime: 2014-10-20 18:52:16 554 | AMAFlags: DOCUMENTATION 555 | UniqueID: 1413823936.3729 556 | LinkedID: 1413823936.3729 557 | Userfield: 558 | Peer: 559 | PeerAccount: 560 | Extra: 561 | 562 | Event: ExtensionStatus 563 | Privilege: call,all 564 | Exten: 2001 565 | Context: default 566 | Hint: SIP/00000 567 | Status: 0 568 | 569 | Event: ExtensionStatus 570 | Privilege: call,all 571 | Exten: *6662 572 | Context: default 573 | Hint: SIP/00000 574 | Status: 0 575 | 576 | Event: QueueMemberStatus 577 | Privilege: agent,all 578 | Queue: maisonvoyance-tous 579 | Location: Local/id-12@agentcallback 580 | MemberName: Agent/999 581 | StateInterface: SIP/00000 582 | Membership: dynamic 583 | Penalty: 0 584 | CallsTaken: 0 585 | LastCall: 0 586 | Status: 1 587 | Paused: 0 588 | Skills: agent-12 589 | 590 | Event: QueueMemberStatus 591 | Privilege: agent,all 592 | Queue: xxxxxxxxxxxx-tous 593 | Location: Local/id-12@agentcallback 594 | MemberName: Agent/999 595 | StateInterface: SIP/00000 596 | Membership: dynamic 597 | Penalty: 0 598 | CallsTaken: 0 599 | LastCall: 0 600 | Status: 1 601 | Paused: 0 602 | Skills: agent-12 603 | 604 | Event: Registry 605 | Privilege: system,all 606 | ChannelType: SIP 607 | Username: 92717277 608 | Domain: xxxxxxxxx.xxx 609 | Status: Registered 610 | -------------------------------------------------------------------------------- /tests/fixtures/asyncagi_channel_does_not_exist.yaml: -------------------------------------------------------------------------------- 1 | Action: AGI 2 | ActionID: action/transaction_uid/1/1 3 | Channel: SIP/eeeeee-00000014 4 | Command: GET VARIABLE DIALSTATUS 5 | CommandID: command/transaction_uid/1/1 6 | 7 | 8 | Response: Error 9 | ActionID: action/transaction_uid/1/1 10 | Message: Channel SIP/eeeeee-00000014 does not exist. 11 | 12 | -------------------------------------------------------------------------------- /tests/fixtures/asyncagi_get_var.yaml: -------------------------------------------------------------------------------- 1 | Action: AGI 2 | ActionID: action/transaction_uid/1/1 3 | Channel: SIP/000000-00000a53 4 | Command: GET VARIABLE endpoint 5 | CommandID: command/transaction_uid/1/1 6 | 7 | Response: Success 8 | ActionID: action/transaction_uid/1/1 9 | Message: Added AGI command to queue 10 | 11 | Event: AGIExec 12 | Privilege: agi,all 13 | SubEvent: Start 14 | Channel: SIP/000000-00000a53 15 | CommandId: 2095882815 16 | Command: GET VARIABLE endpoint 17 | 18 | Event: AGIExec 19 | Privilege: agi,all 20 | SubEvent: End 21 | Channel: SIP/000000-00000a53 22 | CommandId: 2095882815 23 | Command: GET VARIABLE endpoint 24 | ResultCode: 200 25 | Result: Success 26 | 27 | Event: AsyncAGI 28 | Privilege: agi,all 29 | SubEvent: Exec 30 | Channel: SIP/000000-00000a53 31 | CommandID: command/transaction_uid/1/1 32 | Result: 200%20result%3D1%20(SIP%2F000000)%0A 33 | 34 | -------------------------------------------------------------------------------- /tests/fixtures/command_core_show_version.yaml: -------------------------------------------------------------------------------- 1 | action: command 2 | command: core show version 3 | actionid: action/transaction_uid/1/1 4 | 5 | Response: Follows 6 | Privilege: Command 7 | ActionID: action/transaction_uid/1/1 8 | Asterisk 11.11.0+xivo.14.15~20140724.155259.ed5592b-wheezy built by root @ wheezy-farm on a x86_64 running Linux on 2014-08-06 19:30:37 UTC 9 | --END COMMAND-- 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/fixtures/login_failed.yaml: -------------------------------------------------------------------------------- 1 | Asterisk Call Manager/1.3 2 | Action: login 3 | ActionID: action/transaction_uid/1/1 4 | Username: nnnnnnnnn 5 | Secret: nnnnnnnnnn 6 | Events: on 7 | 8 | Response: Error 9 | Message: Authentication failed 10 | ActionID: action/transaction_uid/1/1 11 | 12 | -------------------------------------------------------------------------------- /tests/fixtures/login_md5.yaml: -------------------------------------------------------------------------------- 1 | Asterisk Call Manager/7.0.3 2 | Action: Challenge 3 | AuthType: MD5 4 | ActionID: action/transaction_uid/1/1 5 | 6 | Response: Success 7 | Challenge: 145769581 8 | ActionID: action/transaction_uid/1/1 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/fixtures/login_ok.yaml: -------------------------------------------------------------------------------- 1 | Asterisk Call Manager/1.3 2 | Action: login 3 | ActionID: action/transaction_uid/1/1 4 | username: nnnnnnnnn 5 | secret: yyyyyyyyyyyy 6 | events: on 7 | 8 | Response: Success 9 | ActionID: action/transaction_uid/1/1 10 | Message: Authentication accepted 11 | 12 | Event: FullyBooted 13 | Privilege: system,all 14 | Status: Fully Booted 15 | 16 | -------------------------------------------------------------------------------- /tests/fixtures/logoff.yaml: -------------------------------------------------------------------------------- 1 | 2 | Action: logoff 3 | ActionID: action/transaction_uid/1/1 4 | 5 | Response: Goodbye 6 | ActionID: action/transaction_uid/1/1 7 | Message: Thanks for all the fish. 8 | 9 | -------------------------------------------------------------------------------- /tests/fixtures/originate_sync.yaml: -------------------------------------------------------------------------------- 1 | Action: Originate 2 | ActionID: action/transaction_uid/1/1 3 | Channel: Local/2540 4 | Exten: 2580 5 | Context: default 6 | Priority: 1 7 | Timeout: 30000 8 | CallerID: Panoramisk tests 9 | Async: false 10 | 11 | Response: Success 12 | ActionID: action/transaction_uid/1/1 13 | Message: Originate successfully queued 14 | 15 | -------------------------------------------------------------------------------- /tests/fixtures/ping.yaml: -------------------------------------------------------------------------------- 1 | Response: Success 2 | ActionID: action/transaction_uid/1/1 3 | Ping: Pong 4 | Timestamp: 1409169929.412068 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/fixtures/pjsip_show_endpoint.yaml: -------------------------------------------------------------------------------- 1 | Response: Success 2 | ActionID: action/transaction_uid/1/1 3 | EventList: start 4 | Message: Following are Events for each object associated with the the Endpoint 5 | 6 | Event: EndpointDetail 7 | ActionID: action/transaction_uid/1/1 8 | ObjectType: endpoint 9 | ObjectName: XXXXX 10 | TimersSessExpires: 1800 11 | DeviceStateBusyAt: 0 12 | DtlsCipher: 13 | FromDomain: 14 | DtlsRekey: 0 15 | DtlsFingerprint: SHA-256 16 | DirectMediaMethod: invite 17 | SendRpid: false 18 | PickupGroup: 19 | SdpSession: ALLOcloud v3.0 20 | DtlsVerify: No 21 | MessageContext: 22 | Mailboxes: YYYY 23 | NamedPickupGroup: 24 | RecordOnFeature: automixmon 25 | DtlsPrivateKey: 26 | NamedCallGroup: 27 | T38UdptlMaxdatagram: 0 28 | MediaEncryptionOptimistic: false 29 | Aors: XXXXX 30 | RpidImmediate: false 31 | OutboundProxy: 32 | IdentifyBy: username 33 | InbandProgress: false 34 | RtpSymmetric: true 35 | Transport: tcp_5060 36 | RtpKeepalive: 0 37 | T38UdptlEc: none 38 | FaxDetect: false 39 | T38UdptlNat: false 40 | AllowTransfer: true 41 | TosVideo: 0 42 | SrtpTag32: false 43 | TimersMinSe: 90 44 | CallGroup: 45 | SubMinExpiry: 0 46 | 100rel: yes 47 | DirectMedia: false 48 | RtpTimeoutHold: 0 49 | G726NonStandard: false 50 | DtmfMode: rfc4733 51 | RtpTimeout: 0 52 | DtlsCertFile: 53 | MediaEncryption: no 54 | MediaUseReceivedTransport: false 55 | DirectMediaGlareMitigation: none 56 | TrustIdInbound: false 57 | ForceAvp: false 58 | RecordOffFeature: automixmon 59 | SendDiversion: true 60 | Language: en_US 61 | MwiFromUser: 62 | RtpIpv6: false 63 | IceSupport: false 64 | Callerid: "XXXX" 65 | AggregateMwi: true 66 | OneTouchRecording: false 67 | CosVideo: 0 68 | Accountcode: 69 | Allow: (alaw|opus|speex|vp8|h264) 70 | RewriteContact: true 71 | T38UdptlIpv6: false 72 | ToneZone: 73 | UserEqPhone: false 74 | AllowSubscribe: true 75 | RtpEngine: asterisk 76 | Auth: XXXXX 77 | FromUser: 78 | DisableDirectMediaOnNat: false 79 | UsePtime: false 80 | OutboundAuth: 81 | MediaAddress: 82 | TosAudio: 0 83 | DtlsCaPath: 84 | DtlsSetup: active 85 | ForceRport: true 86 | ConnectedLineMethod: invite 87 | CalleridTag: 88 | Timers: yes 89 | SdpOwner: - 90 | TrustIdOutbound: false 91 | UseAvpf: false 92 | Context: default 93 | MohSuggest: default 94 | SendPai: false 95 | T38Udptl: false 96 | DtlsCaFile: 97 | CalleridPrivacy: allowed_not_screened 98 | CosAudio: 0 99 | DeviceState: Not in use 100 | ActiveChannels: 101 | 102 | Event: AuthDetail 103 | ActionID: action/transaction_uid/1/1 104 | ObjectType: auth 105 | ObjectName: XXXXX 106 | Md5Cred: 107 | Realm: asterisk 108 | AuthType: userpass 109 | Password: ZZZZZZZZZZZ 110 | NonceLifetime: 32 111 | Username: XXXXX 112 | EndpointName: XXXXX 113 | 114 | Event: TransportDetail 115 | ActionID: action/transaction_uid/1/1 116 | ObjectType: transport 117 | ObjectName: tcp_5060 118 | WebsocketWriteTimeout: 100 119 | Cos: 0 120 | Protocol: tcp 121 | Domain: 122 | VerifyServer: No 123 | Bind: 0.0.0.0:5060 124 | PrivKeyFile: 125 | Method: unspecified 126 | RequireClientCert: No 127 | Tos: 0 128 | CaListPath: 129 | Cipher: 130 | ExternalSignalingPort: 0 131 | Password: 132 | CertFile: 133 | VerifyClient: No 134 | ExternalSignalingAddress: 135 | AsyncOperations: 1 136 | ExternalMediaAddress: 137 | CaListFile: 138 | LocalNet: 139 | EndpointName: XXXXX 140 | 141 | Event: AorDetail 142 | ActionID: action/transaction_uid/1/1 143 | ObjectType: aor 144 | ObjectName: XXXXX 145 | SupportPath: false 146 | DefaultExpiration: 3600 147 | QualifyTimeout: 3.000000 148 | Mailboxes: 149 | MinimumExpiration: 60 150 | OutboundProxy: 151 | MaximumExpiration: 7200 152 | QualifyFrequency: 0 153 | AuthenticateQualify: false 154 | Contacts: XXXXX/sip:XXXXX@A.B.C.D:46365;transport=TCP;rinstance=RRRRRR 155 | MaxContacts: 10 156 | RemoveExisting: false 157 | TotalContacts: 1 158 | ContactsRegistered: 1 159 | EndpointName: XXXXX 160 | 161 | Event: ContactStatusDetail 162 | ActionID: action/transaction_uid/1/1 163 | AOR: XXXXX 164 | URI: sip:XXXXX@A.B.C.D:46365;transport=TCP;rinstance=RRRRRR 165 | Status: Unknown 166 | RoundtripUsec: N/A 167 | EndpointName: XXXXX 168 | 169 | Event: EndpointDetailComplete 170 | ActionID: action/transaction_uid/1/1 171 | EventList: Complete 172 | ListItems: 5 173 | -------------------------------------------------------------------------------- /tests/fixtures/queue_add.yaml: -------------------------------------------------------------------------------- 1 | Response: Success 2 | ActionID: action/transaction_uid/1/1 3 | Message: Added interface to queue 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/fixtures/queue_status.yaml: -------------------------------------------------------------------------------- 1 | Response: Success 2 | ActionID: action/transaction_uid/1/1 3 | Message: Queue status will follow 4 | 5 | Event: QueueParams 6 | Queue: xxxxxxxxxxxxxxxx-tous 7 | Max: 0 8 | Strategy: ringall 9 | Calls: 0 10 | Holdtime: 3 11 | TalkTime: 303 12 | Completed: 60 13 | Abandoned: 5 14 | ServiceLevel: 0 15 | ServicelevelPerf: 0.0 16 | Weight: 0 17 | ActionID: action/transaction_uid/1/1 18 | 19 | Event: QueueMember 20 | Queue: xxxxxxxxxxxxxxxx-tous 21 | Name: Agent/220 22 | Location: Local/id-9@agentcallback 23 | StateInterface: SIP/iiiiiiiiiii 24 | Membership: dynamic 25 | Penalty: 0 26 | CallsTaken: 2 27 | LastCall: 1413884970 28 | Status: 1 29 | Paused: 0 30 | Skills: agent-9 31 | ActionID: action/transaction_uid/1/1 32 | 33 | Event: QueueMember 34 | Queue: xxxxxxxxxxxxxxxx-tous 35 | Name: Agent/213 36 | Location: Local/id-3@agentcallback 37 | StateInterface: SIP/ijfptg 38 | Membership: dynamic 39 | Penalty: 0 40 | CallsTaken: 0 41 | LastCall: 0 42 | Status: 1 43 | Paused: 0 44 | Skills: agent-3 45 | ActionID: action/transaction_uid/1/1 46 | 47 | Event: QueueMember 48 | Queue: xxxxxxxxxxxxxxxx-tous 49 | Name: Agent/859 50 | Location: Local/id-7@agentcallback 51 | StateInterface: SIP/rrrrrrr 52 | Membership: dynamic 53 | Penalty: 0 54 | CallsTaken: 0 55 | LastCall: 0 56 | Status: 1 57 | Paused: 0 58 | Skills: agent-7 59 | ActionID: action/transaction_uid/1/1 60 | 61 | Event: QueueMember 62 | Queue: xxxxxxxxxxxxxxxx-tous 63 | Name: Agent/214 64 | Location: Local/id-4@agentcallback 65 | StateInterface: SIP/6r0vs5 66 | Membership: dynamic 67 | Penalty: 0 68 | CallsTaken: 0 69 | LastCall: 0 70 | Status: 1 71 | Paused: 0 72 | Skills: agent-4 73 | ActionID: action/transaction_uid/1/1 74 | 75 | Event: QueueMember 76 | Queue: xxxxxxxxxxxxxxxx-tous 77 | Name: Agent/333 78 | Location: Local/id-10@agentcallback 79 | StateInterface: SIP/qqqqqq 80 | Membership: dynamic 81 | Penalty: 0 82 | CallsTaken: 8 83 | LastCall: 1413758765 84 | Status: 1 85 | Paused: 0 86 | Skills: agent-10 87 | ActionID: action/transaction_uid/1/1 88 | 89 | Event: QueueMember 90 | Queue: xxxxxxxxxxxxxxxx-tous 91 | Name: Agent/999 92 | Location: Local/id-12@agentcallback 93 | StateInterface: SIP/000000 94 | Membership: dynamic 95 | Penalty: 0 96 | CallsTaken: 0 97 | LastCall: 0 98 | Status: 5 99 | Paused: 1 100 | Skills: agent-12 101 | ActionID: action/transaction_uid/1/1 102 | 103 | Event: QueueStatusComplete 104 | ActionID: action/transaction_uid/1/1 105 | 106 | 107 | -------------------------------------------------------------------------------- /tests/fixtures/rasterisk_command.yaml: -------------------------------------------------------------------------------- 1 | Action: Command 2 | ActionID: generated_id 3 | Command: core show channels 4 | 5 | Response: Follows 6 | Privilege: Command 7 | ActionID: generated_id 8 | Channel Location State Application(Data) 9 | 0 active channels 10 | 0 active calls 11 | 2 calls processed 12 | --END COMMAND-- 13 | 14 | -------------------------------------------------------------------------------- /tests/fixtures/sip_notify.yaml: -------------------------------------------------------------------------------- 1 | Action: SIPnotify 2 | Channel: SIP/nnnnnn 3 | Variable: Content= 4 | Variable: Content=This is a test for pushing text to a phone. 5 | Variable: Content=Push test 6 | Variable: Content= 7 | Variable: Content= 8 | Variable: Content-Type=application/xml 9 | Variable: Event=Yealink-xml 10 | 11 | Response: Success 12 | Message: Notify Sent 13 | 14 | -------------------------------------------------------------------------------- /tests/test_fast_agi.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | from panoramisk.exceptions import AGIAppError 4 | from panoramisk.fast_agi import Application 5 | 6 | FAST_AGI_PAYLOAD = b'''agi_network: yes 7 | agi_network_script: call_waiting 8 | agi_request: agi://127.0.0.1:4574/call_waiting 9 | agi_channel: SIP/xxxxxx-00000000 10 | agi_language: en_US 11 | agi_type: SIP 12 | agi_uniqueid: 1437920906.0 13 | agi_version: asterisk 14 | agi_callerid: 201 15 | agi_calleridname: user 201 16 | agi_callingpres: 0 17 | agi_callingani2: 0 18 | agi_callington: 0 19 | agi_callingtns: 0 20 | agi_dnid: 9011 21 | agi_rdnis: unknown 22 | agi_context: default 23 | agi_extension: 9011 24 | agi_priority: 2 25 | agi_enhanced: 0.0 26 | agi_accountcode: default 27 | agi_threadid: -1260881040 28 | agi_arg_1: answered 29 | 30 | ''' 31 | 32 | FAST_AGI_ERROR_PAYLOAD = b'''agi_network: yes 33 | agi_network_script: invalid 34 | agi_request: agi://127.0.0.1:4574/invalid 35 | agi_channel: SIP/xxxxxx-00000000 36 | agi_language: en_US 37 | agi_type: SIP 38 | agi_uniqueid: 1437920906.0 39 | agi_version: asterisk 40 | agi_callerid: 201 41 | agi_calleridname: user 201 42 | agi_callingpres: 0 43 | agi_callingani2: 0 44 | agi_callington: 0 45 | agi_callingtns: 0 46 | agi_dnid: 9011 47 | agi_rdnis: unknown 48 | agi_context: default 49 | agi_extension: 9011 50 | agi_priority: 2 51 | agi_enhanced: 0.0 52 | agi_accountcode: default 53 | agi_threadid: -1260881040 54 | agi_arg_1: answered 55 | 56 | ''' 57 | 58 | 59 | async def call_waiting(request): 60 | r = await request.send_command('ANSWER') 61 | v = {'msg': '', 'result': ('0', ''), 62 | 'status_code': 200} 63 | assert r == v 64 | 65 | 66 | async def invalid(request): 67 | with pytest.raises(AGIAppError) as excinfo: 68 | await request.send_command('EXEC Dial something') 69 | assert excinfo.result == {'msg': '', 'status_code': 200, 'result': ('-1', '')} 70 | 71 | 72 | async def fake_asterisk_client(unused_tcp_port, err=False): 73 | reader, writer = await asyncio.open_connection( 74 | '127.0.0.1', unused_tcp_port) 75 | # send headers 76 | if err: 77 | writer.write(FAST_AGI_ERROR_PAYLOAD) 78 | else: 79 | writer.write(FAST_AGI_PAYLOAD) 80 | # read it back 81 | msg = await reader.readline() 82 | if msg == b'ANSWER\n': 83 | writer.write(b'100 Trying...\n') 84 | writer.write(b'200 result=0\n') 85 | elif msg == b'EXEC Dial something\n': 86 | writer.write(b'200 result=-1\n') 87 | writer.close() 88 | return msg 89 | 90 | 91 | @pytest.mark.asyncio 92 | async def test_fast_agi_application(event_loop, unused_tcp_port): 93 | fa_app = Application(loop=event_loop) 94 | fa_app.add_route('call_waiting', call_waiting) 95 | 96 | server = await asyncio.start_server(fa_app.handler, '127.0.0.1', 97 | unused_tcp_port) 98 | 99 | msg_back = await fake_asterisk_client(unused_tcp_port) 100 | assert msg_back == b'ANSWER\n' 101 | 102 | server.close() 103 | await server.wait_closed() 104 | await asyncio.sleep(1) # Wait the end of endpoint 105 | 106 | 107 | @pytest.mark.asyncio 108 | async def test_fast_agi_application_error(event_loop, unused_tcp_port): 109 | fa_app = Application(loop=event_loop, raise_on_error=True) 110 | fa_app.add_route('invalid', invalid) 111 | 112 | server = await asyncio.start_server(fa_app.handler, '127.0.0.1', 113 | unused_tcp_port) 114 | 115 | await fake_asterisk_client(unused_tcp_port, err=True) 116 | 117 | server.close() 118 | await server.wait_closed() 119 | await asyncio.sleep(1) # Wait the end of endpoint 120 | -------------------------------------------------------------------------------- /tests/test_manager_with_asterisk.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import subprocess 3 | 4 | import pytest 5 | 6 | from panoramisk import Manager 7 | 8 | try: 9 | subprocess.check_call( 10 | ['docker-compose', 'version'], 11 | stdout=subprocess.PIPE, 12 | stderr=subprocess.STDOUT, 13 | ) 14 | compose_available = True 15 | except Exception: 16 | compose_available = False 17 | 18 | pytestmark = pytest.mark.skipif( 19 | not compose_available, 20 | reason='Compose is not available' 21 | ) 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_reconnection_without_lost(event_loop, asterisk): 26 | manager = Manager(loop=event_loop, 27 | username='username', 28 | secret='mysecret') 29 | asterisk.start() 30 | pid = asterisk.proc.pid 31 | 32 | await manager.connect() 33 | await manager.send_action({'Action': 'Ping'}) 34 | 35 | asterisk.stop() 36 | 37 | manager.send_action({'Action': 'Ping'}) 38 | f = manager.send_action({'Action': 'Status'}) 39 | 40 | await asyncio.sleep(.1) 41 | assert manager.awaiting_actions 42 | asterisk.start() 43 | assert pid != asterisk.proc.pid 44 | assert manager.awaiting_actions 45 | await asyncio.sleep(.5) 46 | assert not manager.awaiting_actions 47 | messages = [] 48 | async for message in f: 49 | messages.append(message) 50 | assert message.eventlist.lower() in ("start", "complete"), message 51 | assert len(messages) 52 | -------------------------------------------------------------------------------- /tests/test_manager_with_fixtures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import pytest 4 | import asyncio 5 | from panoramisk import testing 6 | from panoramisk import message 7 | 8 | test_dir = os.path.join(os.path.dirname(__file__), 'fixtures') 9 | 10 | 11 | @pytest.fixture 12 | def manager(request, event_loop): 13 | def manager(stream=None, **config): 14 | config['loop'] = event_loop 15 | if stream: 16 | config['stream'] = os.path.join(test_dir, stream) 17 | return testing.Manager(**config) 18 | return manager 19 | 20 | 21 | def test_ping(manager): 22 | manager = manager(stream='ping.yaml') 23 | future = manager.send_action({'Action': 'Ping'}) 24 | assert 'ping' in future.result() 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_login_ok(manager): 29 | manager = manager(username='xx', secret='xx', stream='login_ok.yaml') 30 | authenticated = await manager.authenticated_future 31 | assert authenticated.success is True 32 | assert manager.login(manager.authenticated_future) is True 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_login_failed(manager): 37 | manager = manager(username='xx', secret='xx', stream='login_failed.yaml') 38 | authenticated = await manager.authenticated_future 39 | assert authenticated.success is False 40 | assert manager.login(manager.authenticated_future) is False 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_login_md5(manager): 45 | manager = manager(username='xx', secret='xx', auth_type='md5', stream='login_md5.yaml') 46 | challenge = await manager.auth_challenge_future 47 | assert len(challenge.challenge) == 9 48 | assert challenge.success is True 49 | assert manager.login(manager.auth_challenge_future) is True 50 | 51 | 52 | def test_logoff(manager): 53 | manager = manager(stream='logoff.yaml') 54 | future = manager.send_action({'Action': 'logoff'}) 55 | assert future.result().success is True 56 | 57 | 58 | def test_queue_status(manager): 59 | manager = manager(stream='queue_status.yaml') 60 | future = manager.send_action({'Action': 'QueueStatus', 61 | 'Queue': 'xxxxxxxxxxxxxxxx-tous'}) 62 | responses = future.result() 63 | assert len(responses) == 9 64 | 65 | 66 | def test_queue_add(manager): 67 | manager = manager(stream='queue_add.yaml') 68 | future = manager.send_action({'Action': 'QueueAdd', 69 | 'Queue': 'xxxxxxxxxxxxxxxx-tous', 70 | 'Interface': 'SIP/000000'}) 71 | responses = future.result() 72 | assert len(responses) == 4 73 | 74 | 75 | def test_pjsip_show_endpoint(manager): 76 | manager = manager(stream='pjsip_show_endpoint.yaml') 77 | future = manager.send_action({'Action': 'PJSIPShowEndpoint', 78 | 'Endpoint': 'XXXXX'}) 79 | responses = future.result() 80 | assert len(responses) == 7 81 | 82 | 83 | def test_command_core_show_version(manager): 84 | manager = manager(stream='command_core_show_version.yaml') 85 | future = manager.send_command('core show version') 86 | responses = future.result() 87 | assert len(responses) == 4 88 | # @todo: in responses['content'], 89 | # you retrieve only '--END COMMAND--' instead of the result of the command 90 | 91 | 92 | def test_asyncagi_get_variable(manager): 93 | manager = manager(stream='asyncagi_get_var.yaml') 94 | future = manager.send_agi_command( 95 | 'SIP/000000-00000a53', 'GET VARIABLE endpoint') 96 | response = future.result()[-1] 97 | assert response.result == '200 result=1 (SIP/000000)' 98 | pretty_result = response.parsed_result() 99 | assert pretty_result['status_code'] == 200 100 | assert pretty_result['result'][0] == '1' 101 | assert pretty_result['result'][1] == 'SIP/000000' 102 | 103 | 104 | def test_asyncagi_get_variable_on_dead_channel(manager): 105 | manager = manager(stream='asyncagi_channel_does_not_exist.yaml') 106 | future = manager.send_agi_command( 107 | 'SIP/eeeeee-00000014', 'GET VARIABLE DIALSTATUS') 108 | response = future.result() 109 | 110 | assert response.response == 'Error' 111 | assert response.message == 'Channel SIP/eeeeee-00000014 does not exist.' 112 | 113 | 114 | def test_originate_sync(manager): 115 | manager = manager(stream='originate_sync.yaml') 116 | future = manager.send_action({'Action': 'Originate', 'Async': 'false'}) 117 | response = future.result() 118 | assert response.success 119 | assert response.message == 'Originate successfully queued' 120 | 121 | 122 | def test_close(manager): 123 | manager().close() 124 | 125 | 126 | def test_events(manager): 127 | manager = manager() 128 | 129 | future = manager.loop.create_future() 130 | 131 | @manager.register_event('Peer*') 132 | def callback(manager, event): 133 | future.set_result(event) 134 | 135 | event = message.Message.from_line('Event: PeerStatus') 136 | assert event.success is True 137 | assert event['Event'] == 'PeerStatus' 138 | assert 'Event' in event 139 | matches = manager.dispatch(event) 140 | assert matches == ['Peer*'] 141 | assert event is future.result() 142 | 143 | event = message.Message.from_line('Event: NoPeerStatus') 144 | matches = manager.dispatch(event) 145 | assert matches == [] 146 | 147 | 148 | def test_coroutine_events_handler(manager): 149 | async def callback(manager, event): 150 | # to create quickly a coroutine generator, don't do that on 151 | # production code 152 | return 153 | 154 | manager = manager() 155 | manager.register_event('Peer*', callback) 156 | event = message.Message.from_line('Event: PeerStatus') 157 | assert event.success is True 158 | assert event['Event'] == 'PeerStatus' 159 | assert 'Event' in event 160 | matches = manager.dispatch(event) 161 | assert matches == ['Peer*'] 162 | 163 | 164 | def test_from_config(event_loop, tmpdir): 165 | f = tmpdir.mkdir("config").join("config.ini") 166 | f.write(''' 167 | [asterisk] 168 | host = 127.0.0.1 169 | user= username 170 | secret = mysecret 171 | ''') 172 | manager = testing.Manager.from_config(str(f), loop=event_loop) 173 | assert manager.config['secret'] == 'mysecret' 174 | 175 | with open(str(f)) as fd: 176 | manager = testing.Manager.from_config(fd, loop=event_loop) 177 | assert manager.config['secret'] == 'mysecret' 178 | 179 | 180 | def test_pinger(manager): 181 | manager = manager() 182 | assert isinstance(manager.pinger, asyncio.TimerHandle) 183 | manager.close() 184 | assert manager.pinger is None 185 | -------------------------------------------------------------------------------- /tests/test_message.py: -------------------------------------------------------------------------------- 1 | from panoramisk.message import Message 2 | from panoramisk import utils 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def message(): 8 | def _message(data): 9 | return Message.from_line(data) 10 | EOL = utils.EOL 11 | utils.EOL = '\n' 12 | yield _message 13 | utils.EOL = EOL 14 | 15 | 16 | def test_multivalue(message): 17 | m = message(''' 18 | Event: X 19 | Value: X 20 | Value: Y 21 | ''') 22 | assert m.value == ['X', 'Y'] 23 | 24 | 25 | def test_content(message): 26 | m = message('''\ 27 | Response: Follows 28 | --- blah --- 29 | ''') 30 | assert m.content == '--- blah ---' 31 | 32 | 33 | @pytest.mark.parametrize('msg', [ 34 | Message( 35 | { 36 | 'Response': 'Success', 37 | 'ChanVariable': ['FROM_DID=', 'SIPURI=sip:42@10.10.10.1:4242'], 38 | } 39 | ), 40 | Message( 41 | { 42 | 'Response': 'Success', 43 | 'ChanVariable': 'var', 44 | } 45 | ) 46 | ] 47 | ) 48 | def test_getdict(msg): 49 | assert isinstance(msg.getdict('chanvariable'), utils.CaseInsensitiveDict) 50 | for k, v in msg.getdict('chanvariable').items(): 51 | assert isinstance(k, str) 52 | assert isinstance(v, str) 53 | -------------------------------------------------------------------------------- /tests/test_protocol.py: -------------------------------------------------------------------------------- 1 | from panoramisk import testing 2 | import asyncio 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def conn(request, event_loop): 8 | 9 | def callback(*args): 10 | pass 11 | 12 | loop = asyncio.get_event_loop_policy().new_event_loop() 13 | manager = testing.Manager(loop=loop) 14 | manager.register_event('Peer*', callback) 15 | return manager.protocol 16 | 17 | 18 | def test_received(conn): 19 | conn.data_received(b'Event: None\r\n\r\n') 20 | conn.data_received(b'Event: PeerStatus\r\nPeer: gawel\r\n\r\n') 21 | conn.data_received(b'Response: Follows\r\nPeer: gawel\r\n\r\n') 22 | conn.data_received(b'Response: Success\r\nPing: Pong\r\n\r\n') 23 | 24 | 25 | def test_send(conn): 26 | assert isinstance(conn.send({}), asyncio.Future) 27 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from panoramisk import utils 2 | from panoramisk.exceptions import AGIException 3 | 4 | 5 | def test_parse_agi_valid_result(): 6 | res = try_parse_agi_result('200 result=0') 7 | assert res == {'msg': '', 'result': ('0', ''), 'status_code': 200} 8 | 9 | res = try_parse_agi_result('200 result=1') 10 | assert res == {'msg': '', 'result': ('1', ''), 'status_code': 200} 11 | 12 | res = try_parse_agi_result('200 result=1234') 13 | assert res == {'msg': '', 'result': ('1234', ''), 'status_code': 200} 14 | 15 | res = try_parse_agi_result('200 result= (timeout)') 16 | assert res == {'msg': '', 'result': ('', 'timeout'), 'status_code': 200} 17 | 18 | 19 | def test_parse_agi_invalid_result(): 20 | res = try_parse_agi_result('510 Invalid or unknown command') 21 | assert res == {'msg': '510 Invalid or unknown command', 22 | 'error': 'AGIInvalidCommand', 23 | 'status_code': 510} 24 | 25 | res = try_parse_agi_result('520 Use this') 26 | assert res == {'msg': '520 Use this', 27 | 'error': 'AGIUsageError', 28 | 'status_code': 520} 29 | 30 | 31 | def try_parse_agi_result(result): 32 | try: 33 | res = utils.parse_agi_result(result) 34 | except AGIException as err: 35 | res = err.items 36 | res['error'] = err.__class__.__name__ 37 | res['msg'] = err.args[0] 38 | 39 | return res 40 | -------------------------------------------------------------------------------- /tools/README.rst: -------------------------------------------------------------------------------- 1 | 2 | Developer 3 | --------- 4 | 5 | When developping with Panoramisk you might find useful to see precisely the requests/responses 6 | going in and out through the AMI interface. 7 | 8 | You can use Tcpdump, Wireshark which might be advised on a production/loaded system, but for 9 | some quick test on your local machine you can use netcat to inspect traffic simple. 10 | 11 | The shell script `netcat-middleman.sh` help you to log your incoming and outgoing traffic. 12 | -------------------------------------------------------------------------------- /tools/netcat-middleman.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | host=localhost 4 | port=5039 5 | if [ "$1" != "" ]; then 6 | host=$1 7 | port=5038 8 | fi; 9 | 10 | while true 11 | do 12 | BACKUP_EXT=$(date +%y%m%d%h%m%s) 13 | if [[ -f outgoing.log ]]; then 14 | cp outgoing.log outgoing.log.$BACKUP_EXT 15 | fi 16 | if [[ -f incoming.log ]]; then 17 | cp incoming.log incoming.log.$BACKUP_EXT 18 | fi 19 | rm pipe 20 | mkfifo pipe 21 | echo "Listening on localhost:$port" 22 | echo "Capture incoming and outgoing traffic from $host:5038" 23 | nc -l -p $port < pipe | tee outgoing.log | nc $host 5038 | tee pipe incoming.log 24 | done 25 | -------------------------------------------------------------------------------- /tools/travis: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | tox -- -sv 6 | 7 | # run only if we are in a pyXX TOXENV 8 | if [ "${TOXENV:0:2}" == "py" ] 9 | then 10 | coverage combine 11 | coverage report --show-missing 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,py310,py311,py312,flake8,docs 3 | 4 | [flake8] 5 | max-line-length = 120 6 | 7 | [testenv] 8 | skip_install=true 9 | commands = 10 | pytest -sxv [] 11 | deps = 12 | -e .[test] 13 | 14 | [testenv:flake8] 15 | basepython = python3.10 16 | commands = 17 | flake8 panoramisk examples 18 | deps = 19 | flake8 20 | 21 | [testenv:docs] 22 | skip_install=false 23 | skipsdist=true 24 | basepython = python3.10 25 | changedir = docs 26 | allowlist_externals = 27 | rm 28 | deps = 29 | sphinx 30 | Pygments 31 | commands = 32 | rm -Rf {envtmpdir}/doctrees {envtmpdir}/html 33 | sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 34 | 35 | --------------------------------------------------------------------------------