├── .coveragerc ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── other.xml ├── runConfigurations │ ├── Unit_Tests.xml │ └── Unit_Tests__coverage_.xml ├── scopes │ └── scope_settings.xml ├── testrunner.xml └── vcs.xml ├── CHANGELOG.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── ari ├── __init__.py ├── client.py └── model.py ├── ari_test ├── __init__.py ├── client_test.py ├── utils.py └── websocket_test.py ├── ast-ari-py.iml ├── examples ├── bridge_example.py ├── cleanup_example.py ├── example.py ├── originate_example.py └── playback_example.py ├── nose.cfg ├── sample-api ├── README.md ├── applications.json ├── asterisk.json ├── bridges.json ├── channels.json ├── deviceStates.json ├── endpoints.json ├── events.json ├── mailboxes.json ├── playbacks.json ├── recordings.json ├── resources.json └── sounds.json ├── setup.cfg └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | exclude_lines = 3 | def __repr__ 4 | raise AssertionError 5 | raise NotImplementedError 6 | if __name__ == .__main__.: 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg* 2 | *.pyc 3 | .coverage 4 | cover/ 5 | dist/ 6 | nosetests.xml 7 | build 8 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | workspace.xml 2 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | ast-ari-py 2 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Unit_Tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Unit_Tests__coverage_.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/testrunner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 0.1.3 (2014-09-08) 2 | ------------------ 3 | 4 | - Allow arbitrary data to be passed to event callbacks 5 | - Fixed misspelling bug in deviceStates resource 6 | 7 | 0.1.2 (2014-08-12) 8 | ------------------ 9 | 10 | - Added support for the deviceStates resource 11 | - Added support for the mailboxes resource 12 | - Renamed playback resource to playbacks (matches change in Asterisk) 13 | 14 | 0.1.1 (2013-10-28) 15 | ------------------ 16 | 17 | - Adding author_email to setup.py 18 | 19 | 0.1.0 (2013-10-28) 20 | ------------------ 21 | 22 | - Initial release 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Digium, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials 14 | provided with the distribution. 15 | 16 | The name of Digium, Inc., or the name of any Contributor, 17 | may not be used to endorse or promote products derived from this 18 | software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS NOT FAULT TOLERANT AND SHOULD NOT BE USED IN ANY 21 | SITUATION ENDANGERING HUMAN LIFE OR PROPERTY. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | OF THE POSSIBILITY OF SUCH DAMAGE. 35 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | include CHANGELOG.rst 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | About 2 | ----- 3 | 4 | This package contains the Python client library for the Asterisk REST 5 | Interface. It builds upon the 6 | `Swagger.py `__ library, providing an 7 | improved, Asterisk-specific API over the API generated by Swagger.py 8 | 9 | Usage 10 | ----- 11 | 12 | Install from source using the ``setup.py`` script. 13 | 14 | :: 15 | 16 | $ sudo ./setup.py install 17 | 18 | 19 | API 20 | === 21 | 22 | An ARI client can be created simply by the ``ari.connect`` method. This will 23 | create a client based on the Swagger API downloaded from Asterisk. 24 | 25 | The API is modeled into the Repository Pattern, as you would find in Domain 26 | Driven Design. Each Swagger Resource (a.k.a. API declaration) is mapped into a 27 | Repository object, which is provided as a field on the client 28 | (``client.channels``, ``client.bridges``). 29 | 30 | Responses from Asterisk are mapped into first-class objects, akin to Domain 31 | Objects in the Repository Pattern. These are provided both on the responses 32 | to RESTful API calls, and for fields from events received over the WebSocket. 33 | 34 | Making REST calls 35 | ================= 36 | 37 | Each Repository Object provides methods which invoke the non-instance specific 38 | operations of the associated Swagger resource (``bridges.list()``, 39 | ``channels.get()``). Instance specific methods are also provided, which require 40 | identity parameters to be passed along (``channels.get(channelId=id)``). 41 | 42 | Instance specific methods are also provided on the Domain Objects 43 | (``some_channel.hangup()``). 44 | 45 | Registering event callbacks 46 | =========================== 47 | 48 | Asterisk may send asyncronous messages over a WebSocket to indicate events of 49 | interest to the application. 50 | 51 | The ``Client`` object has an ``on_event`` method, which can be used to 52 | subscribe for specific events from Asterisk. 53 | 54 | The first-class objects also have 'on_event' methods, which can subscribe to 55 | Stasis events relating to that object. 56 | 57 | Object lifetime 58 | =============== 59 | 60 | The Repository Objects exist for the lifetime of the client that owns them. 61 | 62 | Domain Objects are ephemeral, and not tied to the lifetime of the underlying 63 | object in Asterisk. Pratically, this means that if you call 64 | ``channels.get('1234')`` several times, you may get a different object back 65 | every time. 66 | 67 | You may hold onto an instance of a Domain Object, but you should consider it 68 | to be stale. The data contained in the object may be out of date, but the 69 | methods on the object should still behave properly. 70 | 71 | If you invoke a method on a stale Domain Object that no longer exists in 72 | Asterisk, you will get a HTTPError exception (404 Not Found). 73 | 74 | Caveats 75 | ======= 76 | 77 | The dynamic methods exposed by Repository and Domain objects are, effectively, 78 | remote procedure calls. The current implementation is synchronous, which means 79 | that if anything were to happen to slow responses (slow network, packet loss, 80 | system load, etc.), then the entire application could be affected. 81 | 82 | Examples 83 | ======== 84 | 85 | .. code:: Python 86 | 87 | import ari 88 | 89 | client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo') 90 | 91 | def on_dtmf(channel, event): 92 | digit = event['digit'] 93 | if digit == '#': 94 | channel.play(media='sound:goodbye') 95 | channel.continueInDialplan() 96 | elif digit == '*': 97 | channel.play(media='sound:asterisk-friend') 98 | else: 99 | channel.play(media='sound:digits/%s' % digit) 100 | 101 | 102 | def on_start(channel, event): 103 | channel.on_event('ChannelDtmfReceived', on_dtmf) 104 | channel.answer() 105 | channel.play(media='sound:hello-world') 106 | 107 | 108 | client.on_channel_event('StasisStart', on_start) 109 | client.run(apps="hello") 110 | 111 | 112 | 113 | Development 114 | ----------- 115 | 116 | The code is documented using `Sphinx `__, which 117 | allows `IntelliJ IDEA `__ 118 | to do a better job at inferring types for autocompletion. 119 | 120 | To keep things isolated, I also recommend installing (and using) 121 | `virtualenv `__. 122 | 123 | :: 124 | 125 | $ sudo pip install virtualenv 126 | $ mkdir -p ~/virtualenv 127 | $ virtualenv ~/virtualenv/ari 128 | $ . ~/virtualenv/ari/bin/activate 129 | 130 | `Setuptools `__ is used for 131 | building. `Nose `__ is used 132 | for unit testing, with the `coverage 133 | `__ plugin installed to 134 | generated code coverage reports. Pass ``--with-coverage`` to generate 135 | the code coverage report. HTML versions of the reports are put in 136 | ``cover/index.html``. 137 | 138 | :: 139 | 140 | $ ./setup.py develop # prep for development (install deps, launchers, etc.) 141 | $ ./setup.py nosetests # run unit tests 142 | $ ./setup.py bdist_egg # build distributable 143 | 144 | TODO 145 | ==== 146 | 147 | * Create asynchronous bindings that can be used with Twisted, Tornado, etc. 148 | * Add support for Python 3 149 | 150 | License 151 | ------- 152 | 153 | Copyright (c) 2013-2014, Digium, Inc. All rights reserved. 154 | 155 | Swagger.py is licensed with a `BSD 3-Clause 156 | License `__. 157 | -------------------------------------------------------------------------------- /ari/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2013, Digium, Inc. 3 | # 4 | 5 | """ARI client library 6 | """ 7 | 8 | import ari.client 9 | import swaggerpy.http_client 10 | import urlparse 11 | 12 | Client = client.Client 13 | 14 | 15 | def connect(base_url, username, password): 16 | """Helper method for easily connecting to ARI. 17 | 18 | :param base_url: Base URL for Asterisk HTTP server (http://localhost:8088/) 19 | :param username: ARI username 20 | :param password: ARI password. 21 | :return: 22 | """ 23 | split = urlparse.urlsplit(base_url) 24 | http_client = swaggerpy.http_client.SynchronousHttpClient() 25 | http_client.set_basic_auth(split.hostname, username, password) 26 | return Client(base_url, http_client) 27 | -------------------------------------------------------------------------------- /ari/client.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2013, Digium, Inc. 3 | # 4 | 5 | """ARI client library. 6 | """ 7 | 8 | import json 9 | import logging 10 | import urlparse 11 | import swaggerpy.client 12 | 13 | from ari.model import * 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | class Client(object): 19 | """ARI Client object. 20 | 21 | :param base_url: Base URL for accessing Asterisk. 22 | :param http_client: HTTP client interface. 23 | """ 24 | 25 | def __init__(self, base_url, http_client): 26 | url = urlparse.urljoin(base_url, "ari/api-docs/resources.json") 27 | 28 | self.swagger = swaggerpy.client.SwaggerClient( 29 | url, http_client=http_client) 30 | self.repositories = { 31 | name: Repository(self, name, api) 32 | for (name, api) in self.swagger.resources.items()} 33 | 34 | # Extract models out of the events resource 35 | events = [api['api_declaration'] 36 | for api in self.swagger.api_docs['apis'] 37 | if api['name'] == 'events'] 38 | if events: 39 | self.event_models = events[0]['models'] 40 | else: 41 | self.event_models = {} 42 | 43 | self.websockets = set() 44 | self.event_listeners = {} 45 | self.exception_handler = \ 46 | lambda ex: log.exception("Event listener threw exception") 47 | 48 | def __getattr__(self, item): 49 | """Exposes repositories as fields of the client. 50 | 51 | :param item: Field name 52 | """ 53 | repo = self.get_repo(item) 54 | if not repo: 55 | raise AttributeError( 56 | "'%r' object has no attribute '%s'" % (self, item)) 57 | return repo 58 | 59 | def close(self): 60 | """Close this ARI client. 61 | 62 | This method will close any currently open WebSockets, and close the 63 | underlying Swaggerclient. 64 | """ 65 | for ws in self.websockets: 66 | ws.send_close() 67 | self.swagger.close() 68 | 69 | def get_repo(self, name): 70 | """Get a specific repo by name. 71 | 72 | :param name: Name of the repo to get 73 | :return: Repository, or None if not found. 74 | :rtype: ari.model.Repository 75 | """ 76 | return self.repositories.get(name) 77 | 78 | def __run(self, ws): 79 | """Drains all messages from a WebSocket, sending them to the client's 80 | listeners. 81 | 82 | :param ws: WebSocket to drain. 83 | """ 84 | # TypeChecker false positive on iter(callable, sentinel) -> iterator 85 | # Fixed in plugin v3.0.1 86 | # noinspection PyTypeChecker 87 | for msg_str in iter(lambda: ws.recv(), None): 88 | msg_json = json.loads(msg_str) 89 | if not isinstance(msg_json, dict) or 'type' not in msg_json: 90 | log.error("Invalid event: %s" % msg_str) 91 | continue 92 | 93 | listeners = list(self.event_listeners.get(msg_json['type'], [])) 94 | for listener in listeners: 95 | # noinspection PyBroadException 96 | try: 97 | callback, args, kwargs = listener 98 | args = args or () 99 | kwargs = kwargs or {} 100 | callback(msg_json, *args, **kwargs) 101 | except Exception as e: 102 | self.exception_handler(e) 103 | 104 | def run(self, apps): 105 | """Connect to the WebSocket and begin processing messages. 106 | 107 | This method will block until all messages have been received from the 108 | WebSocket, or until this client has been closed. 109 | 110 | :param apps: Application (or list of applications) to connect for 111 | :type apps: str or list of str 112 | """ 113 | if isinstance(apps, list): 114 | apps = ','.join(apps) 115 | ws = self.swagger.events.eventWebsocket(app=apps) 116 | self.websockets.add(ws) 117 | try: 118 | self.__run(ws) 119 | finally: 120 | ws.close() 121 | self.websockets.remove(ws) 122 | 123 | def on_event(self, event_type, event_cb, *args, **kwargs): 124 | """Register callback for events with given type. 125 | 126 | :param event_type: String name of the event to register for. 127 | :param event_cb: Callback function 128 | :type event_cb: (dict) -> None 129 | :param args: Arguments to pass to event_cb 130 | :param kwargs: Keyword arguments to pass to event_cb 131 | """ 132 | listeners = self.event_listeners.setdefault(event_type, list()) 133 | for cb in listeners: 134 | if event_cb == cb[0]: 135 | listeners.remove(cb) 136 | callback_obj = (event_cb, args, kwargs) 137 | listeners.append(callback_obj) 138 | client = self 139 | 140 | class EventUnsubscriber(object): 141 | """Class to allow events to be unsubscribed. 142 | """ 143 | 144 | def close(self): 145 | """Unsubscribe the associated event callback. 146 | """ 147 | if callback_obj in client.event_listeners[event_type]: 148 | client.event_listeners[event_type].remove(callback_obj) 149 | 150 | return EventUnsubscriber() 151 | 152 | def on_object_event(self, event_type, event_cb, factory_fn, model_id, 153 | *args, **kwargs): 154 | """Register callback for events with the given type. Event fields of 155 | the given model_id type are passed along to event_cb. 156 | 157 | If multiple fields of the event have the type model_id, a dict is 158 | passed mapping the field name to the model object. 159 | 160 | :param event_type: String name of the event to register for. 161 | :param event_cb: Callback function 162 | :type event_cb: (Obj, dict) -> None or (dict[str, Obj], dict) -> 163 | :param factory_fn: Function for creating Obj from JSON 164 | :param model_id: String id for Obj from Swagger models. 165 | :param args: Arguments to pass to event_cb 166 | :param kwargs: Keyword arguments to pass to event_cb 167 | """ 168 | # Find the associated model from the Swagger declaration 169 | event_model = self.event_models.get(event_type) 170 | if not event_model: 171 | raise ValueError("Cannot find event model '%s'" % event_type) 172 | 173 | # Extract the fields that are of the expected type 174 | obj_fields = [k for (k, v) in event_model['properties'].items() 175 | if v['type'] == model_id] 176 | if not obj_fields: 177 | raise ValueError("Event model '%s' has no fields of type %s" 178 | % (event_type, model_id)) 179 | 180 | def extract_objects(event, *args, **kwargs): 181 | """Extract objects of a given type from an event. 182 | 183 | :param event: Event 184 | :param args: Arguments to pass to the event callback 185 | :param kwargs: Keyword arguments to pass to the event 186 | callback 187 | """ 188 | # Extract the fields which are of the expected type 189 | obj = {obj_field: factory_fn(self, event[obj_field]) 190 | for obj_field in obj_fields 191 | if event.get(obj_field)} 192 | # If there's only one field in the schema, just pass that along 193 | if len(obj_fields) == 1: 194 | if obj: 195 | obj = obj.values()[0] 196 | else: 197 | obj = None 198 | event_cb(obj, event, *args, **kwargs) 199 | 200 | return self.on_event(event_type, extract_objects, 201 | *args, 202 | **kwargs) 203 | 204 | def on_channel_event(self, event_type, fn, *args, **kwargs): 205 | """Register callback for Channel related events 206 | 207 | :param event_type: String name of the event to register for. 208 | :param fn: Callback function 209 | :type fn: (Channel, dict) -> None or (list[Channel], dict) -> None 210 | :param args: Arguments to pass to fn 211 | :param kwargs: Keyword arguments to pass to fn 212 | """ 213 | return self.on_object_event(event_type, fn, Channel, 'Channel', 214 | *args, **kwargs) 215 | 216 | def on_bridge_event(self, event_type, fn, *args, **kwargs): 217 | """Register callback for Bridge related events 218 | 219 | :param event_type: String name of the event to register for. 220 | :param fn: Callback function 221 | :type fn: (Bridge, dict) -> None or (list[Bridge], dict) -> None 222 | :param args: Arguments to pass to fn 223 | :param kwargs: Keyword arguments to pass to fn 224 | """ 225 | return self.on_object_event(event_type, fn, Bridge, 'Bridge', 226 | *args, **kwargs) 227 | 228 | def on_playback_event(self, event_type, fn, *args, **kwargs): 229 | """Register callback for Playback related events 230 | 231 | :param event_type: String name of the event to register for. 232 | :param fn: Callback function 233 | :type fn: (Playback, dict) -> None or (list[Playback], dict) -> None 234 | :param args: Arguments to pass to fn 235 | :param kwargs: Keyword arguments to pass to fn 236 | """ 237 | return self.on_object_event(event_type, fn, Playback, 'Playback', 238 | *args, **kwargs) 239 | 240 | def on_live_recording_event(self, event_type, fn, *args, **kwargs): 241 | """Register callback for LiveRecording related events 242 | 243 | :param event_type: String name of the event to register for. 244 | :param fn: Callback function 245 | :type fn: (LiveRecording, dict) -> None or (list[LiveRecording], dict) -> None 246 | :param args: Arguments to pass to fn 247 | :param kwargs: Keyword arguments to pass to fn 248 | """ 249 | return self.on_object_event(event_type, fn, LiveRecording, 250 | 'LiveRecording', *args, **kwargs) 251 | 252 | def on_stored_recording_event(self, event_type, fn, *args, **kwargs): 253 | """Register callback for StoredRecording related events 254 | 255 | :param event_type: String name of the event to register for. 256 | :param fn: Callback function 257 | :type fn: (StoredRecording, dict) -> None or (list[StoredRecording], dict) -> None 258 | :param args: Arguments to pass to fn 259 | :param kwargs: Keyword arguments to pass to fn 260 | """ 261 | return self.on_object_event(event_type, fn, StoredRecording, 262 | 'StoredRecording', *args, **kwargs) 263 | 264 | def on_endpoint_event(self, event_type, fn, *args, **kwargs): 265 | """Register callback for Endpoint related events 266 | 267 | :param event_type: String name of the event to register for. 268 | :param fn: Callback function 269 | :type fn: (Endpoint, dict) -> None or (list[Endpoint], dict) -> None 270 | :param args: Arguments to pass to fn 271 | :param kwargs: Keyword arguments to pass to fn 272 | """ 273 | return self.on_object_event(event_type, fn, Endpoint, 'Endpoint', 274 | *args, **kwargs) 275 | 276 | def on_device_state_event(self, event_type, fn, *args, **kwargs): 277 | """Register callback for DeviceState related events 278 | 279 | :param event_type: String name of the event to register for. 280 | :param fn: Callback function 281 | :type fn: (DeviceState, dict) -> None or (list[DeviceState], dict) -> None 282 | :param args: Arguments to pass to fn 283 | :param kwargs: Keyword arguments to pass to fn 284 | """ 285 | return self.on_object_event(event_type, fn, DeviceState, 'DeviceState', 286 | *args, **kwargs) 287 | 288 | def on_sound_event(self, event_type, fn, *args, **kwargs): 289 | """Register callback for Sound related events 290 | 291 | :param event_type: String name of the event to register for. 292 | :param fn: Sound function 293 | :type fn: (Sound, dict) -> None or (list[Sound], dict) -> None 294 | :param args: Arguments to pass to fn 295 | :param kwargs: Keyword arguments to pass to fn 296 | """ 297 | return self.on_object_event(event_type, fn, Sound, 'Sound', 298 | *args, **kwargs) 299 | 300 | -------------------------------------------------------------------------------- /ari/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Model for mapping ARI Swagger resources and operations into objects. 4 | 5 | The API is modeled into the Repository pattern, as you would find in Domain 6 | Driven Design. 7 | 8 | Each Swagger Resource (a.k.a. API declaration) is mapped into a Repository 9 | object, which has the non-instance specific operations (just like what you 10 | would find in a repository object). 11 | 12 | Responses from operations are mapped into first-class objects, which themselves 13 | have methods which map to instance specific operations (just like what you 14 | would find in a domain object). 15 | 16 | The first-class objects also have 'on_event' methods, which can subscribe to 17 | Stasis events relating to that object. 18 | """ 19 | 20 | import re 21 | import requests 22 | import logging 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | class Repository(object): 28 | """ARI repository. 29 | 30 | This repository maps to an ARI Swagger resource. The operations on the 31 | Swagger resource are mapped to methods on this object, using the 32 | operation's nickname. 33 | 34 | :param client: ARI client. 35 | :type client: client.Client 36 | :param name: Repository name. Maps to the basename of the resource's 37 | .json file 38 | :param resource: Associated Swagger resource. 39 | :type resource: swaggerpy.client.Resource 40 | """ 41 | 42 | def __init__(self, client, name, resource): 43 | self.client = client 44 | self.name = name 45 | self.api = resource 46 | 47 | def __repr__(self): 48 | return "Repository(%s)" % self.name 49 | 50 | def __getattr__(self, item): 51 | """Maps resource operations to methods on this object. 52 | 53 | :param item: Item name. 54 | """ 55 | oper = getattr(self.api, item, None) 56 | if not (hasattr(oper, '__call__') and hasattr(oper, 'json')): 57 | raise AttributeError( 58 | "'%r' object has no attribute '%s'" % (self, item)) 59 | 60 | # The returned function wraps the underlying operation, promoting the 61 | # received HTTP response to a first class object. 62 | return lambda **kwargs: promote(self.client, oper(**kwargs), oper.json) 63 | 64 | 65 | class ObjectIdGenerator(object): 66 | """Interface for extracting identifying information from an object's JSON 67 | representation. 68 | """ 69 | 70 | def get_params(self, obj_json): 71 | """Gets the paramater values for specifying this object in a query. 72 | 73 | :param obj_json: Instance data. 74 | :type obj_json: dict 75 | :return: Dictionary with paramater names and values 76 | :rtype: dict of str, str 77 | """ 78 | raise NotImplementedError("Not implemented") 79 | 80 | def id_as_str(self, obj_json): 81 | """Gets a single string identifying an object. 82 | 83 | :param obj_json: Instance data. 84 | :type obj_json: dict 85 | :return: Id string. 86 | :rtype: str 87 | """ 88 | raise NotImplementedError("Not implemented") 89 | 90 | 91 | # noinspection PyDocstring 92 | class DefaultObjectIdGenerator(ObjectIdGenerator): 93 | """Id generator that works for most of our objects. 94 | 95 | :param param_name: Name of the parameter to specify in queries. 96 | :param id_field: Name of the field to specify in JSON. 97 | """ 98 | 99 | def __init__(self, param_name, id_field='id'): 100 | self.param_name = param_name 101 | self.id_field = id_field 102 | 103 | def get_params(self, obj_json): 104 | return {self.param_name: obj_json[self.id_field]} 105 | 106 | def id_as_str(self, obj_json): 107 | return obj_json[self.id_field] 108 | 109 | 110 | class BaseObject(object): 111 | """Base class for ARI domain objects. 112 | 113 | :param client: ARI client. 114 | :type client: client.Client 115 | :param resource: Associated Swagger resource. 116 | :type resource: swaggerpy.client.Resource 117 | :param as_json: JSON representation of this object instance. 118 | :type as_json: dict 119 | :param event_reg: 120 | """ 121 | 122 | id_generator = ObjectIdGenerator() 123 | 124 | def __init__(self, client, resource, as_json, event_reg): 125 | self.client = client 126 | self.api = resource 127 | self.json = as_json 128 | self.id = self.id_generator.id_as_str(as_json) 129 | self.event_reg = event_reg 130 | 131 | def __repr__(self): 132 | return "%s(%s)" % (self.__class__.__name__, self.id) 133 | 134 | def __getattr__(self, item): 135 | """Promote resource operations related to a single resource to methods 136 | on this class. 137 | 138 | :param item: 139 | """ 140 | oper = getattr(self.api, item, None) 141 | if not (hasattr(oper, '__call__') and hasattr(oper, 'json')): 142 | raise AttributeError( 143 | "'%r' object has no attribute '%r'" % (self, item)) 144 | 145 | def enrich_operation(**kwargs): 146 | """Enriches an operation by specifying parameters specifying this 147 | object's id (i.e., channelId=self.id), and promotes HTTP response 148 | to a first-class object. 149 | 150 | :param kwargs: Operation parameters 151 | :return: First class object mapped from HTTP response. 152 | """ 153 | # Add id to param list 154 | kwargs.update(self.id_generator.get_params(self.json)) 155 | return promote(self.client, oper(**kwargs), oper.json) 156 | 157 | return enrich_operation 158 | 159 | def on_event(self, event_type, fn, *args, **kwargs): 160 | """Register event callbacks for this specific domain object. 161 | 162 | :param event_type: Type of event to register for. 163 | :type event_type: str 164 | :param fn: Callback function for events. 165 | :type fn: (object, dict) -> None 166 | :param args: Arguments to pass to fn 167 | :param kwargs: Keyword arguments to pass to fn 168 | """ 169 | 170 | def fn_filter(objects, event, *args, **kwargs): 171 | """Filter received events for this object. 172 | 173 | :param objects: Objects found in this event. 174 | :param event: Event. 175 | """ 176 | if isinstance(objects, dict): 177 | if self.id in [c.id for c in objects.values()]: 178 | fn(objects, event, *args, **kwargs) 179 | else: 180 | if self.id == objects.id: 181 | fn(objects, event, *args, **kwargs) 182 | 183 | if not self.event_reg: 184 | msg = "Event callback registration called on object with no events" 185 | raise RuntimeError(msg) 186 | 187 | return self.event_reg(event_type, fn_filter, *args, **kwargs) 188 | 189 | 190 | class Channel(BaseObject): 191 | """First class object API. 192 | 193 | :param client: ARI client. 194 | :type client: client.Client 195 | :param channel_json: Instance data 196 | """ 197 | 198 | id_generator = DefaultObjectIdGenerator('channelId') 199 | 200 | def __init__(self, client, channel_json): 201 | super(Channel, self).__init__( 202 | client, client.swagger.channels, channel_json, 203 | client.on_channel_event) 204 | 205 | 206 | class Bridge(BaseObject): 207 | """First class object API. 208 | 209 | :param client: ARI client. 210 | :type client: client.Client 211 | :param bridge_json: Instance data 212 | """ 213 | 214 | id_generator = DefaultObjectIdGenerator('bridgeId') 215 | 216 | def __init__(self, client, bridge_json): 217 | super(Bridge, self).__init__( 218 | client, client.swagger.bridges, bridge_json, 219 | client.on_bridge_event) 220 | 221 | 222 | class Playback(BaseObject): 223 | """First class object API. 224 | 225 | :param client: ARI client. 226 | :type client: client.Client 227 | :param playback_json: Instance data 228 | """ 229 | id_generator = DefaultObjectIdGenerator('playbackId') 230 | 231 | def __init__(self, client, playback_json): 232 | super(Playback, self).__init__( 233 | client, client.swagger.playbacks, playback_json, 234 | client.on_playback_event) 235 | 236 | 237 | class LiveRecording(BaseObject): 238 | """First class object API. 239 | 240 | :param client: ARI client 241 | :type client: client.Client 242 | :param recording_json: Instance data 243 | """ 244 | id_generator = DefaultObjectIdGenerator('recordingName', id_field='name') 245 | 246 | def __init__(self, client, recording_json): 247 | super(LiveRecording, self).__init__( 248 | client, client.swagger.recordings, recording_json, 249 | client.on_live_recording_event) 250 | 251 | 252 | class StoredRecording(BaseObject): 253 | """First class object API. 254 | 255 | :param client: ARI client 256 | :type client: client.Client 257 | :param recording_json: Instance data 258 | """ 259 | id_generator = DefaultObjectIdGenerator('recordingName', id_field='name') 260 | 261 | def __init__(self, client, recording_json): 262 | super(StoredRecording, self).__init__( 263 | client, client.swagger.recordings, recording_json, 264 | client.on_stored_recording_event) 265 | 266 | 267 | # noinspection PyDocstring 268 | class EndpointIdGenerator(ObjectIdGenerator): 269 | """Id generator for endpoints, because they are weird. 270 | """ 271 | 272 | def get_params(self, obj_json): 273 | return { 274 | 'tech': obj_json['technology'], 275 | 'resource': obj_json['resource'] 276 | } 277 | 278 | def id_as_str(self, obj_json): 279 | return "%(tech)s/%(resource)s" % self.get_params(obj_json) 280 | 281 | 282 | class Endpoint(BaseObject): 283 | """First class object API. 284 | 285 | :param client: ARI client. 286 | :type client: client.Client 287 | :param endpoint_json: Instance data 288 | """ 289 | id_generator = EndpointIdGenerator() 290 | 291 | def __init__(self, client, endpoint_json): 292 | super(Endpoint, self).__init__( 293 | client, client.swagger.endpoints, endpoint_json, 294 | client.on_endpoint_event) 295 | 296 | 297 | class DeviceState(BaseObject): 298 | """First class object API. 299 | 300 | :param client: ARI client. 301 | :type client: client.Client 302 | :param endpoint_json: Instance data 303 | """ 304 | id_generator = DefaultObjectIdGenerator('deviceName', id_field='name') 305 | 306 | def __init__(self, client, device_state_json): 307 | super(DeviceState, self).__init__( 308 | client, client.swagger.deviceStates, device_state_json, 309 | client.on_device_state_event) 310 | 311 | 312 | class Sound(BaseObject): 313 | """First class object API. 314 | 315 | :param client: ARI client. 316 | :type client: client.Client 317 | :param sound_json: Instance data 318 | """ 319 | 320 | id_generator = DefaultObjectIdGenerator('soundId') 321 | 322 | def __init__(self, client, sound_json): 323 | super(Sound, self).__init__( 324 | client, client.swagger.sounds, sound_json, client.on_sound_event) 325 | 326 | 327 | class Mailbox(BaseObject): 328 | """First class object API. 329 | 330 | :param client: ARI client. 331 | :type client: client.Client 332 | :param mailbox_json: Instance data 333 | """ 334 | 335 | id_generator = DefaultObjectIdGenerator('mailboxName', id_field='name') 336 | 337 | def __init__(self, client, mailbox_json): 338 | super(Mailbox, self).__init__( 339 | client, client.swagger.mailboxes, mailbox_json, None) 340 | 341 | 342 | def promote(client, resp, operation_json): 343 | """Promote a response from the request's HTTP response to a first class 344 | object. 345 | 346 | :param client: ARI client. 347 | :type client: client.Client 348 | :param resp: HTTP resonse. 349 | :type resp: requests.Response 350 | :param operation_json: JSON model from Swagger API. 351 | :type operation_json: dict 352 | :return: 353 | """ 354 | resp.raise_for_status() 355 | 356 | response_class = operation_json['responseClass'] 357 | is_list = False 358 | m = re.match('''List\[(.*)\]''', response_class) 359 | if m: 360 | response_class = m.group(1) 361 | is_list = True 362 | factory = CLASS_MAP.get(response_class) 363 | if factory: 364 | resp_json = resp.json() 365 | if is_list: 366 | return [factory(client, obj) for obj in resp_json] 367 | return factory(client, resp_json) 368 | if resp.status_code == requests.codes.no_content: 369 | return None 370 | log.info("No mapping for %s; returning JSON" % response_class) 371 | return resp.json() 372 | 373 | 374 | CLASS_MAP = { 375 | 'Bridge': Bridge, 376 | 'Channel': Channel, 377 | 'Endpoint': Endpoint, 378 | 'Playback': Playback, 379 | 'LiveRecording': LiveRecording, 380 | 'StoredRecording': StoredRecording, 381 | 'Mailbox': Mailbox, 382 | 'DeviceState': DeviceState, 383 | } 384 | -------------------------------------------------------------------------------- /ari_test/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2013, Digium, Inc. 3 | # 4 | -------------------------------------------------------------------------------- /ari_test/client_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import ari 4 | import httpretty 5 | import json 6 | import requests 7 | import unittest 8 | import urllib 9 | 10 | from ari_test.utils import AriTestCase 11 | 12 | 13 | GET = httpretty.GET 14 | PUT = httpretty.PUT 15 | POST = httpretty.POST 16 | DELETE = httpretty.DELETE 17 | 18 | 19 | # noinspection PyDocstring 20 | class ClientTest(AriTestCase): 21 | def test_docs(self): 22 | fp = urllib.urlopen("http://ari.py/ari/api-docs/resources.json") 23 | try: 24 | actual = json.load(fp) 25 | self.assertEqual(self.BASE_URL, actual['basePath']) 26 | finally: 27 | fp.close() 28 | 29 | def test_empty_listing(self): 30 | self.serve(GET, 'channels', body='[]') 31 | actual = self.uut.channels.list() 32 | self.assertEqual([], actual) 33 | 34 | def test_one_listing(self): 35 | self.serve(GET, 'channels', body='[{"id": "test-channel"}]') 36 | self.serve(DELETE, 'channels', 'test-channel') 37 | 38 | actual = self.uut.channels.list() 39 | self.assertEqual(1, len(actual)) 40 | actual[0].hangup() 41 | 42 | def test_play(self): 43 | self.serve(GET, 'channels', 'test-channel', 44 | body='{"id": "test-channel"}') 45 | self.serve(POST, 'channels', 'test-channel', 'play', 46 | body='{"id": "test-playback"}') 47 | self.serve(DELETE, 'playbacks', 'test-playback') 48 | 49 | channel = self.uut.channels.get(channelId='test-channel') 50 | playback = channel.play(media='sound:test-sound') 51 | playback.stop() 52 | 53 | def test_bad_resource(self): 54 | try: 55 | self.uut.i_am_not_a_resource.list() 56 | self.fail("How did it find that resource?") 57 | except AttributeError: 58 | pass 59 | 60 | def test_bad_repo_method(self): 61 | try: 62 | self.uut.channels.i_am_not_a_method() 63 | self.fail("How did it find that method?") 64 | except AttributeError: 65 | pass 66 | 67 | def test_bad_object_method(self): 68 | self.serve(GET, 'channels', 'test-channel', 69 | body='{"id": "test-channel"}') 70 | 71 | try: 72 | channel = self.uut.channels.get(channelId='test-channel') 73 | channel.i_am_not_a_method() 74 | self.fail("How did it find that method?") 75 | except AttributeError: 76 | pass 77 | 78 | def test_bad_param(self): 79 | try: 80 | self.uut.channels.list(i_am_not_a_param='asdf') 81 | self.fail("How did it find that param?") 82 | except TypeError: 83 | pass 84 | 85 | def test_bad_response(self): 86 | self.serve(GET, 'channels', body='{"message": "This is just a test"}', 87 | status=500) 88 | try: 89 | self.uut.channels.list() 90 | self.fail("Should have thrown an exception") 91 | except requests.HTTPError as e: 92 | self.assertEqual(500, e.response.status_code) 93 | self.assertEqual( 94 | {"message": "This is just a test"}, e.response.json()) 95 | 96 | def test_endpoints(self): 97 | self.serve(GET, 'endpoints', 98 | body='[{"technology": "TEST", "resource": "1234"}]') 99 | self.serve(GET, 'endpoints', 'TEST', '1234', 100 | body='{"technology": "TEST", "resource": "1234"}') 101 | 102 | endpoints = self.uut.endpoints.list() 103 | self.assertEqual(1, len(endpoints)) 104 | endpoint = endpoints[0].get() 105 | self.assertEqual('TEST', endpoint.json['technology']) 106 | self.assertEqual('1234', endpoint.json['resource']) 107 | 108 | def test_live_recording(self): 109 | self.serve(GET, 'recordings', 'live', 'test-recording', 110 | body='{"name": "test-recording"}') 111 | self.serve(DELETE, 'recordings', 'live', 'test-recording', status=204) 112 | 113 | recording = self.uut.recordings.getLive(recordingName='test-recording') 114 | recording.cancel() 115 | 116 | def test_stored_recording(self): 117 | self.serve(GET, 'recordings', 'stored', 'test-recording', 118 | body='{"name": "test-recording"}') 119 | self.serve(DELETE, 'recordings', 'stored', 'test-recording', 120 | status=204) 121 | 122 | recording = self.uut.recordings.getStored( 123 | recordingName='test-recording') 124 | recording.deleteStored() 125 | 126 | def test_mailboxes(self): 127 | self.serve(PUT, 'mailboxes', '1000', 128 | body='{"name": "1000", "old_messages": "1", "new_messages": "3"}') 129 | 130 | mailbox = self.uut.mailboxes.update( 131 | mailboxName='1000', 132 | oldMessages='1', 133 | newMessages='3') 134 | self.assertEqual('1000', mailbox['name']) 135 | self.assertEqual('1', mailbox['old_messages']) 136 | self.assertEqual('3', mailbox['new_messages']) 137 | 138 | def test_device_state(self): 139 | self.serve(PUT, 'deviceStates', 'foobar', 140 | body='{"name": "foobar", "state": "BUSY"}') 141 | device_state = self.uut.deviceStates.update( 142 | deviceName='foobar', 143 | deviceState='BUSY') 144 | self.assertEqual('foobar', device_state['name']) 145 | self.assertEqual('BUSY', device_state['state']) 146 | 147 | def setUp(self): 148 | super(ClientTest, self).setUp() 149 | self.uut = ari.connect('http://ari.py/', 'test', 'test') 150 | 151 | 152 | if __name__ == '__main__': 153 | unittest.main() 154 | -------------------------------------------------------------------------------- /ari_test/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import httpretty 4 | import os 5 | import unittest 6 | import urlparse 7 | import ari 8 | import requests 9 | 10 | 11 | class AriTestCase(unittest.TestCase): 12 | """Base class for mock ARI server. 13 | """ 14 | 15 | BASE_URL = "http://ari.py/ari" 16 | 17 | def setUp(self): 18 | """Setup httpretty; create ARI client. 19 | """ 20 | super(AriTestCase, self).setUp() 21 | httpretty.enable() 22 | self.serve_api() 23 | self.uut = ari.connect('http://ari.py/', 'test', 'test') 24 | 25 | def tearDown(self): 26 | """Cleanup. 27 | """ 28 | super(AriTestCase, self).tearDown() 29 | httpretty.disable() 30 | httpretty.reset() 31 | 32 | @classmethod 33 | def build_url(cls, *args): 34 | """Build a URL, based off of BASE_URL, with the given args. 35 | 36 | >>> AriTestCase.build_url('foo', 'bar', 'bam', 'bang') 37 | 'http://ari.py/ari/foo/bar/bam/bang' 38 | 39 | :param args: URL components 40 | :return: URL 41 | """ 42 | url = cls.BASE_URL 43 | for arg in args: 44 | url = urlparse.urljoin(url + '/', arg) 45 | return url 46 | 47 | def serve_api(self): 48 | """Register all api-docs with httpretty to serve them for unit tests. 49 | """ 50 | for filename in os.listdir('sample-api'): 51 | if filename.endswith('.json'): 52 | with open(os.path.join('sample-api', filename)) as fp: 53 | body = fp.read() 54 | self.serve(httpretty.GET, 'api-docs', filename, body=body) 55 | 56 | def serve(self, method, *args, **kwargs): 57 | """Serve a single URL for current test. 58 | 59 | :param method: HTTP method. httpretty.{GET,PUT,POST,DELETE}. 60 | :param args: URL path segments. 61 | :param kwargs: See httpretty.register_uri() 62 | """ 63 | url = self.build_url(*args) 64 | if kwargs.get('body') is None and 'status' not in kwargs: 65 | kwargs['status'] = requests.codes.no_content 66 | httpretty.register_uri(method, url, 67 | content_type="application/json", 68 | **kwargs) 69 | -------------------------------------------------------------------------------- /ari_test/websocket_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """WebSocket testing. 4 | """ 5 | 6 | import unittest 7 | import ari 8 | import httpretty 9 | 10 | from ari_test.utils import AriTestCase 11 | from swaggerpy.http_client import SynchronousHttpClient 12 | 13 | BASE_URL = "http://ari.py/ari" 14 | 15 | GET = httpretty.GET 16 | PUT = httpretty.PUT 17 | POST = httpretty.POST 18 | DELETE = httpretty.DELETE 19 | 20 | 21 | # noinspection PyDocstring 22 | class WebSocketTest(AriTestCase): 23 | def setUp(self): 24 | super(WebSocketTest, self).setUp() 25 | self.actual = [] 26 | 27 | def record_event(self, event): 28 | self.actual.append(event) 29 | 30 | def test_empty(self): 31 | uut = connect(BASE_URL, []) 32 | uut.on_event('ev', self.record_event) 33 | uut.run('test') 34 | self.assertEqual([], self.actual) 35 | 36 | def test_series(self): 37 | messages = [ 38 | '{"type": "ev", "data": 1}', 39 | '{"type": "ev", "data": 2}', 40 | '{"type": "not_ev", "data": 3}', 41 | '{"type": "not_ev", "data": 5}', 42 | '{"type": "ev", "data": 9}' 43 | ] 44 | uut = connect(BASE_URL, messages) 45 | uut.on_event("ev", self.record_event) 46 | uut.run('test') 47 | expected = [ 48 | {"type": "ev", "data": 1}, 49 | {"type": "ev", "data": 2}, 50 | {"type": "ev", "data": 9} 51 | ] 52 | self.assertEqual(expected, self.actual) 53 | 54 | def test_unsubscribe(self): 55 | messages = [ 56 | '{"type": "ev", "data": 1}', 57 | '{"type": "ev", "data": 2}' 58 | ] 59 | uut = connect(BASE_URL, messages) 60 | self.once_ran = 0 61 | 62 | def only_once(event): 63 | self.once_ran += 1 64 | self.assertEqual(1, event['data']) 65 | self.once.close() 66 | 67 | def both_events(event): 68 | self.record_event(event) 69 | 70 | self.once = uut.on_event("ev", only_once) 71 | self.both = uut.on_event("ev", both_events) 72 | uut.run('test') 73 | 74 | expected = [ 75 | {"type": "ev", "data": 1}, 76 | {"type": "ev", "data": 2} 77 | ] 78 | self.assertEqual(expected, self.actual) 79 | self.assertEqual(1, self.once_ran) 80 | 81 | def test_on_channel(self): 82 | self.serve(DELETE, 'channels', 'test-channel') 83 | messages = [ 84 | '{ "type": "StasisStart", "channel": { "id": "test-channel" } }' 85 | ] 86 | uut = connect(BASE_URL, messages) 87 | 88 | def cb(channel, event): 89 | self.record_event(event) 90 | channel.hangup() 91 | 92 | uut.on_channel_event('StasisStart', cb) 93 | uut.run('test') 94 | 95 | expected = [ 96 | {"type": "StasisStart", "channel": {"id": "test-channel"}} 97 | ] 98 | self.assertEqual(expected, self.actual) 99 | 100 | def test_on_channel_unsubscribe(self): 101 | messages = [ 102 | '{ "type": "StasisStart", "channel": { "id": "test-channel1" } }', 103 | '{ "type": "StasisStart", "channel": { "id": "test-channel2" } }' 104 | ] 105 | uut = connect(BASE_URL, messages) 106 | 107 | def only_once(channel, event): 108 | self.record_event(event) 109 | self.once.close() 110 | 111 | self.once = uut.on_channel_event('StasisStart', only_once) 112 | uut.run('test') 113 | 114 | expected = [ 115 | {"type": "StasisStart", "channel": {"id": "test-channel1"}} 116 | ] 117 | self.assertEqual(expected, self.actual) 118 | 119 | def test_channel_on_event(self): 120 | self.serve(GET, 'channels', 'test-channel', 121 | body='{"id": "test-channel"}') 122 | self.serve(DELETE, 'channels', 'test-channel') 123 | messages = [ 124 | '{"type": "ChannelStateChange", "channel": {"id": "ignore-me"}}', 125 | '{"type": "ChannelStateChange", "channel": {"id": "test-channel"}}' 126 | ] 127 | 128 | uut = connect(BASE_URL, messages) 129 | channel = uut.channels.get(channelId='test-channel') 130 | 131 | def cb(channel, event): 132 | self.record_event(event) 133 | channel.hangup() 134 | 135 | channel.on_event('ChannelStateChange', cb) 136 | uut.run('test') 137 | 138 | expected = [ 139 | {"type": "ChannelStateChange", "channel": {"id": "test-channel"}} 140 | ] 141 | self.assertEqual(expected, self.actual) 142 | 143 | def test_arbitrary_callback_arguments(self): 144 | self.serve(GET, 'channels', 'test-channel', 145 | body='{"id": "test-channel"}') 146 | self.serve(DELETE, 'channels', 'test-channel') 147 | messages = [ 148 | '{"type": "ChannelDtmfReceived", "channel": {"id": "test-channel"}}' 149 | ] 150 | obj = {'key': 'val'} 151 | 152 | uut = connect(BASE_URL, messages) 153 | channel = uut.channels.get(channelId='test-channel') 154 | 155 | def cb(channel, event, arg): 156 | if arg == 'done': 157 | channel.hangup() 158 | else: 159 | self.record_event(arg) 160 | 161 | def cb2(channel, event, arg1, arg2=None, arg3=None): 162 | self.record_event(arg1) 163 | self.record_event(arg2) 164 | self.record_event(arg3) 165 | 166 | channel.on_event('ChannelDtmfReceived', cb, 1) 167 | channel.on_event('ChannelDtmfReceived', cb, arg=2) 168 | channel.on_event('ChannelDtmfReceived', cb, obj) 169 | channel.on_event('ChannelDtmfReceived', cb2, 2.0, arg3=[1, 2, 3]) 170 | channel.on_event('ChannelDtmfReceived', cb, 'done') 171 | uut.run('test') 172 | 173 | expected = [1, 2, obj, 2.0, None, [1, 2, 3]] 174 | self.assertEqual(expected, self.actual) 175 | 176 | def test_bad_event_type(self): 177 | uut = connect(BASE_URL, []) 178 | try: 179 | uut.on_object_event( 180 | 'BadEventType', self.noop, self.noop, 'Channel') 181 | self.fail("Event does not exist") 182 | except ValueError: 183 | pass 184 | 185 | def test_bad_object_type(self): 186 | uut = connect(BASE_URL, []) 187 | try: 188 | uut.on_object_event('StasisStart', self.noop, self.noop, 'Bridge') 189 | self.fail("Event has no bridge") 190 | except ValueError: 191 | pass 192 | 193 | # noinspection PyUnusedLocal 194 | def noop(self, *args, **kwargs): 195 | self.fail("Noop unexpectedly called") 196 | 197 | 198 | class WebSocketStubConnection(object): 199 | """Stub WebSocket connection. 200 | 201 | :param messages: 202 | """ 203 | 204 | def __init__(self, messages): 205 | self.messages = list(messages) 206 | self.messages.reverse() 207 | 208 | def recv(self): 209 | """Fake receive method 210 | 211 | :return: Next message, or None if no more messages. 212 | """ 213 | if self.messages: 214 | return str(self.messages.pop()) 215 | return None 216 | 217 | def send_close(self): 218 | """Fake send_close method 219 | """ 220 | return 221 | 222 | def close(self): 223 | """Fake close method 224 | """ 225 | return 226 | 227 | 228 | class WebSocketStubClient(SynchronousHttpClient): 229 | """Stub WebSocket client. 230 | 231 | :param messages: List of messages to return. 232 | :type messages: list 233 | """ 234 | 235 | def __init__(self, messages): 236 | super(WebSocketStubClient, self).__init__() 237 | self.messages = messages 238 | 239 | def ws_connect(self, url, params=None): 240 | """Fake connect method. 241 | 242 | Returns a WebSocketStubConnection, which itself returns the series of 243 | messages from WebSocketStubClient in its recv() method. 244 | 245 | :param url: Ignored. 246 | :param params: Ignored. 247 | :return: Stub connection. 248 | """ 249 | return WebSocketStubConnection(self.messages) 250 | 251 | 252 | def raise_exceptions(ex): 253 | """Testing exception handler for ARI client. 254 | 255 | :param ex: Exception caught by the event loop. 256 | """ 257 | raise 258 | 259 | 260 | def connect(base_url, messages): 261 | """Connect, with a WebSocket client test double that merely retuns the 262 | series of given messages. 263 | 264 | :param base_url: Base URL for REST calls. 265 | :param messages: Message strings to return from the WebSocket. 266 | :return: ARI client with stubbed WebSocket. 267 | """ 268 | http_client = WebSocketStubClient(messages) 269 | client = ari.Client(base_url, http_client) 270 | client.exception_handler = raise_exceptions 271 | return client 272 | 273 | 274 | if __name__ == '__main__': 275 | unittest.main() 276 | -------------------------------------------------------------------------------- /ast-ari-py.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/bridge_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Short example of how to use bridge objects. 4 | 5 | This example will create a holding bridge (if one doesn't already exist). Any 6 | channels that enter Stasis is placed into the bridge. Whenever a channel 7 | enters the bridge, a tone is played to the bridge. 8 | """ 9 | 10 | # 11 | # Copyright (c) 2013, Digium, Inc. 12 | # 13 | 14 | import ari 15 | 16 | client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo') 17 | 18 | # 19 | # Find (or create) a holding bridge. 20 | # 21 | bridges = [b for b in client.bridges.list() if 22 | b.json['bridge_type'] == 'holding'] 23 | if bridges: 24 | bridge = bridges[0] 25 | print "Using bridge %s" % bridge.id 26 | else: 27 | bridge = client.bridges.create(type='holding') 28 | print "Created bridge %s" % bridge.id 29 | 30 | 31 | def on_enter(bridge, ev): 32 | """Callback for bridge enter events. 33 | 34 | When channels enter the bridge, play tones to the whole bridge. 35 | 36 | :param bridge: Bridge entering the channel. 37 | :param ev: Event. 38 | """ 39 | # ignore announcer channels - see ASTERISK-22744 40 | if ev['channel']['name'].startswith('Announcer/'): 41 | return 42 | bridge.play(media="sound:ascending-2tone") 43 | 44 | 45 | bridge.on_event('ChannelEnteredBridge', on_enter) 46 | 47 | 48 | def stasis_start_cb(channel, ev): 49 | """Callback for StasisStart events. 50 | 51 | For new channels, answer and put them in the holding bridge. 52 | 53 | :param channel: Channel that entered Stasis 54 | :param ev: Event 55 | """ 56 | channel.answer() 57 | bridge.addChannel(channel=channel.id) 58 | 59 | 60 | client.on_channel_event('StasisStart', stasis_start_cb) 61 | 62 | # Run the WebSocket 63 | client.run(apps='hello') 64 | -------------------------------------------------------------------------------- /examples/cleanup_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ARI resources may be closed, if an application only needs them temporarily. 4 | """ 5 | 6 | # 7 | # Copyright (c) 2013, Digium, Inc. 8 | # 9 | 10 | import ari 11 | import logging 12 | import sys 13 | import thread 14 | 15 | logging.basicConfig() 16 | 17 | client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo') 18 | 19 | 20 | # noinspection PyUnusedLocal 21 | def on_start(channel, event): 22 | """Callback for StasisStart events. 23 | 24 | On new channels, register the on_dtmf callback, answer the channel and 25 | play "Hello, world" 26 | 27 | :param channel: Channel DTMF was received from. 28 | :param event: Event. 29 | """ 30 | on_dtmf_handle = None 31 | 32 | def on_dtmf(channel, event): 33 | """Callback for DTMF events. 34 | 35 | When DTMF is received, play the digit back to the channel. # hangs up, 36 | * plays a special message. 37 | 38 | :param channel: Channel DTMF was received from. 39 | :param event: Event. 40 | """ 41 | digit = event['digit'] 42 | if digit == '#': 43 | channel.play(media='sound:goodbye') 44 | channel.continueInDialplan() 45 | on_dtmf_handle.close() 46 | elif digit == '*': 47 | channel.play(media='sound:asterisk-friend') 48 | else: 49 | channel.play(media='sound:digits/%s' % digit) 50 | 51 | on_dtmf_handle = channel.on_event('ChannelDtmfReceived', on_dtmf) 52 | channel.answer() 53 | channel.play(media='sound:hello-world') 54 | 55 | 56 | client.on_channel_event('StasisStart', on_start) 57 | 58 | # Run the WebSocket 59 | sync = thread.allocate_lock() 60 | 61 | 62 | def run(): 63 | """Thread for running the Websocket. 64 | """ 65 | sync.acquire() 66 | client.run(apps="hello") 67 | sync.release() 68 | 69 | 70 | thr = thread.start_new_thread(run, ()) 71 | print "Press enter to exit" 72 | sys.stdin.readline() 73 | client.close() 74 | sync.acquire() 75 | print "Application finished" 76 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Brief example of using the channel API. 4 | 5 | This app will answer any channel sent to Stasis(hello), and play "Hello, 6 | world" to the channel. For any DTMF events received, the number is played back 7 | to the channel. Press # to hang up, and * for a special message. 8 | """ 9 | 10 | # 11 | # Copyright (c) 2013, Digium, Inc. 12 | # 13 | 14 | import ari 15 | 16 | client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo') 17 | 18 | 19 | def on_dtmf(channel, event): 20 | """Callback for DTMF events. 21 | 22 | When DTMF is received, play the digit back to the channel. # hangs up, 23 | * plays a special message. 24 | 25 | :param channel: Channel DTMF was received from. 26 | :param event: Event. 27 | """ 28 | digit = event['digit'] 29 | if digit == '#': 30 | channel.play(media='sound:goodbye') 31 | channel.continueInDialplan() 32 | elif digit == '*': 33 | channel.play(media='sound:asterisk-friend') 34 | else: 35 | channel.play(media='sound:digits/%s' % digit) 36 | 37 | 38 | def on_start(channel, event): 39 | """Callback for StasisStart events. 40 | 41 | On new channels, register the on_dtmf callback, answer the channel and 42 | play "Hello, world" 43 | 44 | :param channel: Channel DTMF was received from. 45 | :param event: Event. 46 | """ 47 | channel.on_event('ChannelDtmfReceived', on_dtmf) 48 | channel.answer() 49 | channel.play(media='sound:hello-world') 50 | 51 | 52 | client.on_channel_event('StasisStart', on_start) 53 | 54 | # Run the WebSocket 55 | client.run(apps="hello") 56 | -------------------------------------------------------------------------------- /examples/originate_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Example demonstrating ARI channel origination. 4 | 5 | """ 6 | 7 | # 8 | # Copyright (c) 2013, Digium, Inc. 9 | # 10 | import requests 11 | 12 | import ari 13 | 14 | from requests import HTTPError 15 | 16 | OUTGOING_ENDPOINT = "SIP/blink" 17 | 18 | client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo') 19 | 20 | # 21 | # Find (or create) a holding bridge. 22 | # 23 | bridges = [b for b in client.bridges.list() 24 | if b.json['bridge_type'] == 'holding'] 25 | if bridges: 26 | holding_bridge = bridges[0] 27 | print "Using bridge %s" % holding_bridge.id 28 | else: 29 | holding_bridge = client.bridges.create(type='holding') 30 | print "Created bridge %s" % holding_bridge.id 31 | 32 | 33 | def safe_hangup(channel): 34 | """Hangup a channel, ignoring 404 errors. 35 | 36 | :param channel: Channel to hangup. 37 | """ 38 | try: 39 | channel.hangup() 40 | except HTTPError as e: 41 | # Ignore 404's, since channels can go away before we get to them 42 | if e.response.status_code != requests.codes.not_found: 43 | raise 44 | 45 | 46 | def on_start(incoming, event): 47 | """Callback for StasisStart events. 48 | 49 | When an incoming channel starts, put it in the holding bridge and 50 | originate a channel to connect to it. When that channel answers, create a 51 | bridge and put both of them into it. 52 | 53 | :param incoming: 54 | :param event: 55 | """ 56 | # Only process channels with the 'incoming' argument 57 | if event['args'] != ['incoming']: 58 | return 59 | 60 | # Answer and put in the holding bridge 61 | incoming.answer() 62 | incoming.play(media="sound:pls-wait-connect-call") 63 | holding_bridge.addChannel(channel=incoming.id) 64 | 65 | # Originate the outgoing channel 66 | outgoing = client.channels.originate( 67 | endpoint=OUTGOING_ENDPOINT, app="hello", appArgs="dialed") 68 | 69 | # If the incoming channel ends, hangup the outgoing channel 70 | incoming.on_event('StasisEnd', lambda *args: safe_hangup(outgoing)) 71 | # and vice versa. If the endpoint rejects the call, it is destroyed 72 | # without entering Stasis() 73 | outgoing.on_event('ChannelDestroyed', 74 | lambda *args: safe_hangup(incoming)) 75 | 76 | def outgoing_on_start(channel, event): 77 | """Callback for StasisStart events on the outgoing channel 78 | 79 | :param channel: Outgoing channel. 80 | :param event: Event. 81 | """ 82 | # Create a bridge, putting both channels into it. 83 | bridge = client.bridges.create(type='mixing') 84 | outgoing.answer() 85 | bridge.addChannel(channel=[incoming.id, outgoing.id]) 86 | # Clean up the bridge when done 87 | outgoing.on_event('StasisEnd', lambda *args: bridge.destroy()) 88 | 89 | outgoing.on_event('StasisStart', outgoing_on_start) 90 | 91 | 92 | client.on_channel_event('StasisStart', on_start) 93 | 94 | # Run the WebSocket 95 | client.run(apps="hello") 96 | -------------------------------------------------------------------------------- /examples/playback_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Example demonstrating using the returned object from an API call. 4 | 5 | This app plays demo-contrats on any channel sent to Stasis(hello). DTMF keys 6 | are used to control the playback. 7 | """ 8 | 9 | # 10 | # Copyright (c) 2013, Digium, Inc. 11 | # 12 | 13 | import ari 14 | import sys 15 | 16 | client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo') 17 | 18 | 19 | def on_start(channel, event): 20 | """Callback for StasisStart events. 21 | 22 | On new channels, answer, play demo-congrats, and register a DTMF listener. 23 | 24 | :param channel: Channel DTMF was received from. 25 | :param event: Event. 26 | """ 27 | channel.answer() 28 | playback = channel.play(media='sound:demo-congrats') 29 | 30 | def on_dtmf(channel, event): 31 | """Callback for DTMF events. 32 | 33 | DTMF events control the playback operation. 34 | 35 | :param channel: Channel DTMF was received on. 36 | :param event: Event. 37 | """ 38 | # Since the callback was registered to a specific channel, we can 39 | # control the playback object we already have in scope. 40 | digit = event['digit'] 41 | if digit == '5': 42 | playback.control(operation='pause') 43 | elif digit == '8': 44 | playback.control(operation='unpause') 45 | elif digit == '4': 46 | playback.control(operation='reverse') 47 | elif digit == '6': 48 | playback.control(operation='forward') 49 | elif digit == '2': 50 | playback.control(operation='restart') 51 | elif digit == '#': 52 | playback.stop() 53 | channel.continueInDialplan() 54 | else: 55 | print >> sys.stderr, "Unknown DTMF %s" % digit 56 | 57 | channel.on_event('ChannelDtmfReceived', on_dtmf) 58 | 59 | 60 | client.on_channel_event('StasisStart', on_start) 61 | 62 | # Run the WebSocket 63 | client.run(apps='hello') 64 | -------------------------------------------------------------------------------- /nose.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | cover-erase = True 3 | cover-html = True 4 | cover-inclusive = True 5 | cover-package = ari 6 | with-doctest = True 7 | doctest-tests = True 8 | with-xunit = True 9 | with-tissue = True 10 | tissue-package = ari_test,ari 11 | logging-level = DEBUG 12 | nocapture = True 13 | no-byte-compile = True 14 | -------------------------------------------------------------------------------- /sample-api/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This directory contains a slimmed down example of the Asterisk REST Interface 4 | Swagger definitions. These are only used for unit testing, so shouldn't be taken 5 | too seriously. 6 | -------------------------------------------------------------------------------- /sample-api/applications.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/applications.{format}", 8 | "apis": [ 9 | { 10 | "path": "/applications", 11 | "description": "Stasis applications", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "summary": "List all applications.", 16 | "nickname": "list", 17 | "responseClass": "List[Application]" 18 | } 19 | ] 20 | }, 21 | { 22 | "path": "/applications/{applicationName}", 23 | "description": "Stasis application", 24 | "operations": [ 25 | { 26 | "httpMethod": "GET", 27 | "summary": "Get details of an application.", 28 | "nickname": "get", 29 | "responseClass": "Application", 30 | "parameters": [ 31 | { 32 | "name": "applicationName", 33 | "description": "Application's name", 34 | "paramType": "path", 35 | "required": true, 36 | "allowMultiple": false, 37 | "dataType": "string" 38 | } 39 | ], 40 | "errorResponses": [ 41 | { 42 | "code": 404, 43 | "reason": "Application does not exist." 44 | } 45 | ] 46 | } 47 | ] 48 | }, 49 | { 50 | "path": "/applications/{applicationName}/subscription", 51 | "description": "Stasis application", 52 | "operations": [ 53 | { 54 | "httpMethod": "POST", 55 | "summary": "Subscribe an application to a event source.", 56 | "notes": "Returns the state of the application after the subscriptions have changed", 57 | "nickname": "subscribe", 58 | "responseClass": "Application", 59 | "parameters": [ 60 | { 61 | "name": "applicationName", 62 | "description": "Application's name", 63 | "paramType": "path", 64 | "required": true, 65 | "allowMultiple": false, 66 | "dataType": "string" 67 | }, 68 | { 69 | "name": "eventSource", 70 | "description": "URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}", 71 | "paramType": "query", 72 | "required": true, 73 | "allowMultiple": true, 74 | "dataType": "string" 75 | } 76 | ], 77 | "errorResponses": [ 78 | { 79 | "code": 400, 80 | "reason": "Missing parameter." 81 | }, 82 | { 83 | "code": 404, 84 | "reason": "Application does not exist." 85 | }, 86 | { 87 | "code": 422, 88 | "reason": "Event source does not exist." 89 | } 90 | ] 91 | }, 92 | { 93 | "httpMethod": "DELETE", 94 | "summary": "Unsubscribe an application from an event source.", 95 | "notes": "Returns the state of the application after the subscriptions have changed", 96 | "nickname": "unsubscribe", 97 | "responseClass": "Application", 98 | "parameters": [ 99 | { 100 | "name": "applicationName", 101 | "description": "Application's name", 102 | "paramType": "path", 103 | "required": true, 104 | "allowMultiple": false, 105 | "dataType": "string" 106 | }, 107 | { 108 | "name": "eventSource", 109 | "description": "URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}", 110 | "paramType": "query", 111 | "required": true, 112 | "allowMultiple": true, 113 | "dataType": "string" 114 | } 115 | ], 116 | "errorResponses": [ 117 | { 118 | "code": 400, 119 | "reason": "Missing parameter; event source scheme not recognized." 120 | }, 121 | { 122 | "code": 404, 123 | "reason": "Application does not exist." 124 | }, 125 | { 126 | "code": 409, 127 | "reason": "Application not subscribed to event source." 128 | }, 129 | { 130 | "code": 422, 131 | "reason": "Event source does not exist." 132 | } 133 | ] 134 | } 135 | ] 136 | } 137 | ], 138 | "models": { 139 | "Application": { 140 | "id": "Application", 141 | "description": "Details of a Stasis application", 142 | "properties": { 143 | "name": { 144 | "type": "string", 145 | "description": "Name of this application", 146 | "required": true 147 | }, 148 | "channel_ids": { 149 | "type": "List[string]", 150 | "description": "Id's for channels subscribed to.", 151 | "required": true 152 | }, 153 | "bridge_ids": { 154 | "type": "List[string]", 155 | "description": "Id's for bridges subscribed to.", 156 | "required": true 157 | }, 158 | "endpoint_ids": { 159 | "type": "List[string]", 160 | "description": "{tech}/{resource} for endpoints subscribed to.", 161 | "required": true 162 | } 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /sample-api/asterisk.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/asterisk.{format}", 8 | "apis": [ 9 | { 10 | "path": "/asterisk/info", 11 | "description": "Asterisk system information (similar to core show settings)", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "summary": "Gets Asterisk system information.", 16 | "nickname": "getInfo", 17 | "responseClass": "AsteriskInfo", 18 | "parameters": [ 19 | { 20 | "name": "only", 21 | "description": "Filter information returned", 22 | "paramType": "query", 23 | "required": false, 24 | "allowMultiple": true, 25 | "dataType": "string", 26 | "allowableValues": { 27 | "valueType": "LIST", 28 | "values": [ 29 | "build", 30 | "system", 31 | "config", 32 | "status" 33 | ] 34 | } 35 | } 36 | ] 37 | } 38 | ] 39 | }, 40 | { 41 | "path": "/asterisk/variable", 42 | "description": "Global variables", 43 | "operations": [ 44 | { 45 | "httpMethod": "GET", 46 | "summary": "Get the value of a global variable.", 47 | "nickname": "getGlobalVar", 48 | "responseClass": "Variable", 49 | "parameters": [ 50 | { 51 | "name": "variable", 52 | "description": "The variable to get", 53 | "paramType": "query", 54 | "required": true, 55 | "allowMultiple": false, 56 | "dataType": "string" 57 | } 58 | ], 59 | "errorResponses": [ 60 | { 61 | "code": 400, 62 | "reason": "Missing variable parameter." 63 | } 64 | ] 65 | }, 66 | { 67 | "httpMethod": "POST", 68 | "summary": "Set the value of a global variable.", 69 | "nickname": "setGlobalVar", 70 | "responseClass": "void", 71 | "parameters": [ 72 | { 73 | "name": "variable", 74 | "description": "The variable to set", 75 | "paramType": "query", 76 | "required": true, 77 | "allowMultiple": false, 78 | "dataType": "string" 79 | }, 80 | { 81 | "name": "value", 82 | "description": "The value to set the variable to", 83 | "paramType": "query", 84 | "required": false, 85 | "allowMultiple": false, 86 | "dataType": "string" 87 | } 88 | ], 89 | "errorResponses": [ 90 | { 91 | "code": 400, 92 | "reason": "Missing variable parameter." 93 | } 94 | ] 95 | } 96 | ] 97 | } 98 | ], 99 | "models": { 100 | "BuildInfo": { 101 | "id": "BuildInfo", 102 | "description": "Info about how Asterisk was built", 103 | "properties": { 104 | "os": { 105 | "required": true, 106 | "type": "string", 107 | "description": "OS Asterisk was built on." 108 | }, 109 | "kernel": { 110 | "required": true, 111 | "type": "string", 112 | "description": "Kernel version Asterisk was built on." 113 | }, 114 | "options": { 115 | "required": true, 116 | "type": "string", 117 | "description": "Compile time options, or empty string if default." 118 | }, 119 | "machine": { 120 | "required": true, 121 | "type": "string", 122 | "description": "Machine architecture (x86_64, i686, ppc, etc.)" 123 | }, 124 | "date": { 125 | "required": true, 126 | "type": "string", 127 | "description": "Date and time when Asterisk was built." 128 | }, 129 | "user": { 130 | "required": true, 131 | "type": "string", 132 | "description": "Username that build Asterisk" 133 | } 134 | } 135 | }, 136 | "SystemInfo": { 137 | "id": "SystemInfo", 138 | "description": "Info about Asterisk", 139 | "properties": { 140 | "version": { 141 | "required": true, 142 | "type": "string", 143 | "description": "Asterisk version." 144 | }, 145 | "entity_id": { 146 | "required": true, 147 | "type": "string", 148 | "description": "" 149 | } 150 | } 151 | }, 152 | "SetId": { 153 | "id": "SetId", 154 | "description": "Effective user/group id", 155 | "properties": { 156 | "user": { 157 | "required": true, 158 | "type": "string", 159 | "description": "Effective user id." 160 | }, 161 | "group": { 162 | "required": true, 163 | "type": "string", 164 | "description": "Effective group id." 165 | } 166 | } 167 | }, 168 | "ConfigInfo": { 169 | "id": "ConfigInfo", 170 | "description": "Info about Asterisk configuration", 171 | "properties": { 172 | "name": { 173 | "required": true, 174 | "type": "string", 175 | "description": "Asterisk system name." 176 | }, 177 | "default_language": { 178 | "required": true, 179 | "type": "string", 180 | "description": "Default language for media playback." 181 | }, 182 | "max_channels": { 183 | "required": false, 184 | "type": "int", 185 | "description": "Maximum number of simultaneous channels." 186 | }, 187 | "max_open_files": { 188 | "required": false, 189 | "type": "int", 190 | "description": "Maximum number of open file handles (files, sockets)." 191 | }, 192 | "max_load": { 193 | "required": false, 194 | "type": "double", 195 | "description": "Maximum load avg on system." 196 | }, 197 | "setid": { 198 | "required": true, 199 | "type": "SetId", 200 | "description": "Effective user/group id for running Asterisk." 201 | } 202 | } 203 | }, 204 | "StatusInfo": { 205 | "id": "StatusInfo", 206 | "description": "Info about Asterisk status", 207 | "properties": { 208 | "startup_time": { 209 | "required": true, 210 | "type": "Date", 211 | "description": "Time when Asterisk was started." 212 | }, 213 | "last_reload_time": { 214 | "required": true, 215 | "type": "Date", 216 | "description": "Time when Asterisk was last reloaded." 217 | } 218 | } 219 | }, 220 | "AsteriskInfo": { 221 | "id": "AsteriskInfo", 222 | "description": "Asterisk system information", 223 | "properties": { 224 | "build": { 225 | "required": false, 226 | "type": "BuildInfo", 227 | "description": "Info about how Asterisk was built" 228 | }, 229 | "system": { 230 | "required": false, 231 | "type": "SystemInfo", 232 | "description": "Info about the system running Asterisk" 233 | }, 234 | "config": { 235 | "required": false, 236 | "type": "ConfigInfo", 237 | "description": "Info about Asterisk configuration" 238 | }, 239 | "status": { 240 | "required": false, 241 | "type": "StatusInfo", 242 | "description": "Info about Asterisk status" 243 | } 244 | } 245 | }, 246 | "Variable": { 247 | "id": "Variable", 248 | "description": "The value of a channel variable", 249 | "properties": { 250 | "value": { 251 | "required": true, 252 | "type": "string", 253 | "description": "The value of the variable requested" 254 | } 255 | } 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /sample-api/bridges.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/bridges.{format}", 8 | "apis": [ 9 | { 10 | "path": "/bridges", 11 | "description": "Active bridges", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "summary": "List all active bridges in Asterisk.", 16 | "nickname": "list", 17 | "responseClass": "List[Bridge]" 18 | }, 19 | { 20 | "httpMethod": "POST", 21 | "summary": "Create a new bridge.", 22 | "notes": "This bridge persists until it has been shut down, or Asterisk has been shut down.", 23 | "nickname": "create", 24 | "responseClass": "Bridge", 25 | "parameters": [ 26 | { 27 | "name": "type", 28 | "description": "Type of bridge to create.", 29 | "paramType": "query", 30 | "required": false, 31 | "allowMultiple": false, 32 | "dataType": "string", 33 | "allowableValues": { 34 | "valueType": "LIST", 35 | "values": [ 36 | "mixing", 37 | "holding" 38 | ] 39 | } 40 | } 41 | ] 42 | } 43 | ] 44 | }, 45 | { 46 | "path": "/bridges/{bridgeId}", 47 | "description": "Individual bridge", 48 | "operations": [ 49 | { 50 | "httpMethod": "GET", 51 | "summary": "Get bridge details.", 52 | "nickname": "get", 53 | "responseClass": "Bridge", 54 | "parameters": [ 55 | { 56 | "name": "bridgeId", 57 | "description": "Bridge's id", 58 | "paramType": "path", 59 | "required": true, 60 | "allowMultiple": false, 61 | "dataType": "string" 62 | } 63 | ], 64 | "errorResponses": [ 65 | { 66 | "code": 404, 67 | "reason": "Bridge not found" 68 | } 69 | ] 70 | }, 71 | { 72 | "httpMethod": "DELETE", 73 | "summary": "Shut down a bridge.", 74 | "notes": "If any channels are in this bridge, they will be removed and resume whatever they were doing beforehand.", 75 | "nickname": "destroy", 76 | "responseClass": "void", 77 | "parameters": [ 78 | { 79 | "name": "bridgeId", 80 | "description": "Bridge's id", 81 | "paramType": "path", 82 | "required": true, 83 | "allowMultiple": false, 84 | "dataType": "string" 85 | } 86 | ], 87 | "errorResponses": [ 88 | { 89 | "code": 404, 90 | "reason": "Bridge not found" 91 | } 92 | ] 93 | } 94 | ] 95 | }, 96 | { 97 | "path": "/bridges/{bridgeId}/addChannel", 98 | "description": "Add a channel to a bridge", 99 | "operations": [ 100 | { 101 | "httpMethod": "POST", 102 | "summary": "Add a channel to a bridge.", 103 | "nickname": "addChannel", 104 | "responseClass": "void", 105 | "parameters": [ 106 | { 107 | "name": "bridgeId", 108 | "description": "Bridge's id", 109 | "paramType": "path", 110 | "required": true, 111 | "allowMultiple": false, 112 | "dataType": "string" 113 | }, 114 | { 115 | "name": "channel", 116 | "description": "Ids of channels to add to bridge", 117 | "paramType": "query", 118 | "required": true, 119 | "allowMultiple": true, 120 | "dataType": "string" 121 | }, 122 | { 123 | "name": "role", 124 | "description": "Channel's role in the bridge", 125 | "paramType": "query", 126 | "required": false, 127 | "allowMultiple": false, 128 | "dataType": "string" 129 | } 130 | ], 131 | "errorResponses": [ 132 | { 133 | "code": 400, 134 | "reason": "Channel not found" 135 | }, 136 | { 137 | "code": 404, 138 | "reason": "Bridge not found" 139 | }, 140 | { 141 | "code": 409, 142 | "reason": "Bridge not in Stasis application" 143 | }, 144 | { 145 | "code": 422, 146 | "reason": "Channel not in Stasis application" 147 | } 148 | ] 149 | } 150 | ] 151 | }, 152 | { 153 | "path": "/bridges/{bridgeId}/removeChannel", 154 | "description": "Remove a channel from a bridge", 155 | "operations": [ 156 | { 157 | "httpMethod": "POST", 158 | "summary": "Remove a channel from a bridge.", 159 | "nickname": "removeChannel", 160 | "responseClass": "void", 161 | "parameters": [ 162 | { 163 | "name": "bridgeId", 164 | "description": "Bridge's id", 165 | "paramType": "path", 166 | "required": true, 167 | "allowMultiple": false, 168 | "dataType": "string" 169 | }, 170 | { 171 | "name": "channel", 172 | "description": "Ids of channels to remove from bridge", 173 | "paramType": "query", 174 | "required": true, 175 | "allowMultiple": true, 176 | "dataType": "string" 177 | } 178 | ], 179 | "errorResponses": [ 180 | { 181 | "code": 400, 182 | "reason": "Channel not found" 183 | }, 184 | { 185 | "code": 404, 186 | "reason": "Bridge not found" 187 | }, 188 | { 189 | "code": 409, 190 | "reason": "Bridge not in Stasis application" 191 | }, 192 | { 193 | "code": 422, 194 | "reason": "Channel not in this bridge" 195 | } 196 | ] 197 | } 198 | ] 199 | }, 200 | { 201 | "path": "/bridges/{bridgeId}/moh", 202 | "description": "Play music on hold to a bridge", 203 | "operations": [ 204 | { 205 | "httpMethod": "POST", 206 | "summary": "Play music on hold to a bridge or change the MOH class that is playing.", 207 | "nickname": "startMoh", 208 | "responseClass": "void", 209 | "parameters": [ 210 | { 211 | "name": "bridgeId", 212 | "description": "Bridge's id", 213 | "paramType": "path", 214 | "required": true, 215 | "allowMultiple": false, 216 | "dataType": "string" 217 | }, 218 | { 219 | "name": "mohClass", 220 | "description": "Channel's id", 221 | "paramType": "query", 222 | "required": false, 223 | "allowMultiple": false, 224 | "dataType": "string" 225 | } 226 | ], 227 | "errorResponses": [ 228 | { 229 | "code": 404, 230 | "reason": "Bridge not found" 231 | }, 232 | { 233 | "code": 409, 234 | "reason": "Bridge not in Stasis application" 235 | } 236 | ] 237 | }, 238 | { 239 | "httpMethod": "DELETE", 240 | "summary": "Stop playing music on hold to a bridge.", 241 | "notes": "This will only stop music on hold being played via POST bridges/{bridgeId}/moh.", 242 | "nickname": "stopMoh", 243 | "responseClass": "void", 244 | "parameters": [ 245 | { 246 | "name": "bridgeId", 247 | "description": "Bridge's id", 248 | "paramType": "path", 249 | "required": true, 250 | "allowMultiple": false, 251 | "dataType": "string" 252 | } 253 | ], 254 | "errorResponses": [ 255 | { 256 | "code": 404, 257 | "reason": "Bridge not found" 258 | }, 259 | { 260 | "code": 409, 261 | "reason": "Bridge not in Stasis application" 262 | } 263 | ] 264 | } 265 | ] 266 | }, 267 | { 268 | "path": "/bridges/{bridgeId}/play", 269 | "description": "Play media to the participants of a bridge", 270 | "operations": [ 271 | { 272 | "httpMethod": "POST", 273 | "summary": "Start playback of media on a bridge.", 274 | "notes": "The media URI may be any of a number of URI's. Currently sound: and recording: URI's are supported. This operation creates a playback resource that can be used to control the playback of media (pause, rewind, fast forward, etc.)", 275 | "nickname": "play", 276 | "responseClass": "Playback", 277 | "parameters": [ 278 | { 279 | "name": "bridgeId", 280 | "description": "Bridge's id", 281 | "paramType": "path", 282 | "required": true, 283 | "allowMultiple": false, 284 | "dataType": "string" 285 | }, 286 | { 287 | "name": "media", 288 | "description": "Media's URI to play.", 289 | "paramType": "query", 290 | "required": true, 291 | "allowMultiple": false, 292 | "dataType": "string" 293 | }, 294 | { 295 | "name": "lang", 296 | "description": "For sounds, selects language for sound.", 297 | "paramType": "query", 298 | "required": false, 299 | "allowMultiple": false, 300 | "dataType": "string" 301 | }, 302 | { 303 | "name": "offsetms", 304 | "description": "Number of media to skip before playing.", 305 | "paramType": "query", 306 | "required": false, 307 | "allowMultiple": false, 308 | "dataType": "int", 309 | "defaultValue": 0, 310 | "allowableValues": { 311 | "valueType": "RANGE", 312 | "min": 0 313 | } 314 | 315 | }, 316 | { 317 | "name": "skipms", 318 | "description": "Number of milliseconds to skip for forward/reverse operations.", 319 | "paramType": "query", 320 | "required": false, 321 | "allowMultiple": false, 322 | "dataType": "int", 323 | "defaultValue": 3000, 324 | "allowableValues": { 325 | "valueType": "RANGE", 326 | "min": 0 327 | } 328 | 329 | } 330 | ], 331 | "errorResponses": [ 332 | { 333 | "code": 404, 334 | "reason": "Bridge not found" 335 | }, 336 | { 337 | "code": 409, 338 | "reason": "Bridge not in a Stasis application" 339 | } 340 | ] 341 | } 342 | ] 343 | }, 344 | { 345 | "path": "/bridges/{bridgeId}/record", 346 | "description": "Record audio on a bridge", 347 | "operations": [ 348 | { 349 | "httpMethod": "POST", 350 | "summary": "Start a recording.", 351 | "notes": "This records the mixed audio from all channels participating in this bridge.", 352 | "nickname": "record", 353 | "responseClass": "LiveRecording", 354 | "parameters": [ 355 | { 356 | "name": "bridgeId", 357 | "description": "Bridge's id", 358 | "paramType": "path", 359 | "required": true, 360 | "allowMultiple": false, 361 | "dataType": "string" 362 | }, 363 | { 364 | "name": "name", 365 | "description": "Recording's filename", 366 | "paramType": "query", 367 | "required": true, 368 | "allowMultiple": false, 369 | "dataType": "string" 370 | }, 371 | { 372 | "name": "format", 373 | "description": "Format to encode audio in", 374 | "paramType": "query", 375 | "required": true, 376 | "allowMultiple": false, 377 | "dataType": "string" 378 | }, 379 | { 380 | "name": "maxDurationSeconds", 381 | "description": "Maximum duration of the recording, in seconds. 0 for no limit.", 382 | "paramType": "query", 383 | "required": false, 384 | "allowMultiple": false, 385 | "dataType": "int", 386 | "defaultValue": 0, 387 | "allowableValues": { 388 | "valueType": "RANGE", 389 | "min": 0 390 | } 391 | }, 392 | { 393 | "name": "maxSilenceSeconds", 394 | "description": "Maximum duration of silence, in seconds. 0 for no limit.", 395 | "paramType": "query", 396 | "required": false, 397 | "allowMultiple": false, 398 | "dataType": "int", 399 | "defaultValue": 0, 400 | "allowableValues": { 401 | "valueType": "RANGE", 402 | "min": 0 403 | } 404 | }, 405 | { 406 | "name": "ifExists", 407 | "description": "Action to take if a recording with the same name already exists.", 408 | "paramType": "query", 409 | "required": false, 410 | "allowMultiple": false, 411 | "dataType": "string", 412 | "defaultValue": "fail", 413 | "allowableValues": { 414 | "valueType": "LIST", 415 | "values": [ 416 | "fail", 417 | "overwrite", 418 | "append" 419 | ] 420 | } 421 | }, 422 | { 423 | "name": "beep", 424 | "description": "Play beep when recording begins", 425 | "paramType": "query", 426 | "required": false, 427 | "allowMultiple": false, 428 | "dataType": "boolean", 429 | "defaultValue": false 430 | }, 431 | { 432 | "name": "terminateOn", 433 | "description": "DTMF input to terminate recording.", 434 | "paramType": "query", 435 | "required": false, 436 | "allowMultiple": false, 437 | "dataType": "string", 438 | "defaultValue": "none", 439 | "allowableValues": { 440 | "valueType": "LIST", 441 | "values": [ 442 | "none", 443 | "any", 444 | "*", 445 | "#" 446 | ] 447 | } 448 | } 449 | ], 450 | "errorResponses": [ 451 | { 452 | "code": 400, 453 | "reason": "Invalid parameters" 454 | }, 455 | { 456 | "code": 404, 457 | "reason": "Bridge not found" 458 | }, 459 | { 460 | "code": 409, 461 | "reason": "Bridge is not in a Stasis application; A recording with the same name already exists on the system and can not be overwritten because it is in progress or ifExists=fail" 462 | }, 463 | { 464 | "code": 422, 465 | "reason": "The format specified is unknown on this system" 466 | } 467 | ] 468 | } 469 | ] 470 | } 471 | ], 472 | "models": { 473 | "Bridge": { 474 | "id": "Bridge", 475 | "description": "The merging of media from one or more channels.\n\nEveryone on the bridge receives the same audio.", 476 | "properties": { 477 | "id": { 478 | "type": "string", 479 | "description": "Unique identifier for this bridge", 480 | "required": true 481 | }, 482 | "technology": { 483 | "type": "string", 484 | "description": "Name of the current bridging technology", 485 | "required": true 486 | }, 487 | "bridge_type": { 488 | "type": "string", 489 | "description": "Type of bridge technology", 490 | "required": true, 491 | "allowableValues": { 492 | "valueType": "LIST", 493 | "values": [ 494 | "mixing", 495 | "holding" 496 | ] 497 | } 498 | }, 499 | "bridge_class": { 500 | "type": "string", 501 | "description": "Bridging class", 502 | "required": true 503 | }, 504 | "channels": { 505 | "type": "List[string]", 506 | "description": "Ids of channels participating in this bridge", 507 | "required": true 508 | } 509 | } 510 | } 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /sample-api/channels.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/channels.{format}", 8 | "apis": [ 9 | { 10 | "path": "/channels", 11 | "description": "Active channels", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "summary": "List all active channels in Asterisk.", 16 | "nickname": "list", 17 | "responseClass": "List[Channel]" 18 | }, 19 | { 20 | "httpMethod": "POST", 21 | "summary": "Create a new channel (originate).", 22 | "notes": "The new channel is created immediately and a snapshot of it returned. If a Stasis application is provided it will be automatically subscribed to the originated channel for further events and updates.", 23 | "nickname": "originate", 24 | "responseClass": "Channel", 25 | "parameters": [ 26 | { 27 | "name": "endpoint", 28 | "description": "Endpoint to call.", 29 | "paramType": "query", 30 | "required": true, 31 | "allowMultiple": false, 32 | "dataType": "string" 33 | }, 34 | { 35 | "name": "extension", 36 | "description": "The extension to dial after the endpoint answers", 37 | "paramType": "query", 38 | "required": false, 39 | "allowMultiple": false, 40 | "dataType": "string" 41 | }, 42 | { 43 | "name": "context", 44 | "description": "The context to dial after the endpoint answers. If omitted, uses 'default'", 45 | "paramType": "query", 46 | "required": false, 47 | "allowMultiple": false, 48 | "dataType": "string" 49 | }, 50 | { 51 | "name": "priority", 52 | "description": "The priority to dial after the endpoint answers. If omitted, uses 1", 53 | "paramType": "query", 54 | "required": false, 55 | "allowMultiple": false, 56 | "dataType": "long" 57 | }, 58 | { 59 | "name": "app", 60 | "description": "The application that is subscribed to the originated channel, and passed to the Stasis application.", 61 | "paramType": "query", 62 | "required": false, 63 | "allowMultiple": false, 64 | "dataType": "string" 65 | }, 66 | { 67 | "name": "appArgs", 68 | "description": "The application arguments to pass to the Stasis application.", 69 | "paramType": "query", 70 | "required": false, 71 | "allowMultiple": false, 72 | "dataType": "string" 73 | }, 74 | { 75 | "name": "callerId", 76 | "description": "CallerID to use when dialing the endpoint or extension.", 77 | "paramType": "query", 78 | "required": false, 79 | "allowMultiple": false, 80 | "dataType": "string" 81 | }, 82 | { 83 | "name": "timeout", 84 | "description": "Timeout (in seconds) before giving up dialing, or -1 for no timeout.", 85 | "paramType": "query", 86 | "required": false, 87 | "allowMultiple": false, 88 | "dataType": "int", 89 | "defaultValue": 30 90 | } 91 | ], 92 | "errorResponses": [ 93 | { 94 | "code": 400, 95 | "reason": "Invalid parameters for originating a channel." 96 | } 97 | ] 98 | } 99 | ] 100 | }, 101 | { 102 | "path": "/channels/{channelId}", 103 | "description": "Active channel", 104 | "operations": [ 105 | { 106 | "httpMethod": "GET", 107 | "summary": "Channel details.", 108 | "nickname": "get", 109 | "responseClass": "Channel", 110 | "parameters": [ 111 | { 112 | "name": "channelId", 113 | "description": "Channel's id", 114 | "paramType": "path", 115 | "required": true, 116 | "allowMultiple": false, 117 | "dataType": "string" 118 | } 119 | ], 120 | "errorResponses": [ 121 | { 122 | "code": 404, 123 | "reason": "Channel not found" 124 | } 125 | ] 126 | }, 127 | { 128 | "httpMethod": "DELETE", 129 | "summary": "Delete (i.e. hangup) a channel.", 130 | "nickname": "hangup", 131 | "responseClass": "void", 132 | "parameters": [ 133 | { 134 | "name": "channelId", 135 | "description": "Channel's id", 136 | "paramType": "path", 137 | "required": true, 138 | "allowMultiple": false, 139 | "dataType": "string" 140 | }, 141 | { 142 | "name": "reason", 143 | "description": "Reason for hanging up the channel", 144 | "paramType": "query", 145 | "required": false, 146 | "allowMultiple": false, 147 | "dataType": "string", 148 | "defalutValue": "normal", 149 | "allowableValues": { 150 | "valueType": "LIST", 151 | "values": [ 152 | "normal", 153 | "busy", 154 | "congestion" 155 | ] 156 | } 157 | } 158 | ], 159 | "errorResponses": [ 160 | { 161 | "code": 400, 162 | "reason": "Invalid reason for hangup provided" 163 | }, 164 | { 165 | "code": 404, 166 | "reason": "Channel not found" 167 | } 168 | ] 169 | } 170 | ] 171 | }, 172 | { 173 | "path": "/channels/{channelId}/continue", 174 | "description": "Exit application; continue execution in the dialplan", 175 | "operations": [ 176 | { 177 | "httpMethod": "POST", 178 | "summary": "Exit application; continue execution in the dialplan.", 179 | "nickname": "continueInDialplan", 180 | "responseClass": "void", 181 | "parameters": [ 182 | { 183 | "name": "channelId", 184 | "description": "Channel's id", 185 | "paramType": "path", 186 | "required": true, 187 | "allowMultiple": false, 188 | "dataType": "string" 189 | }, 190 | { 191 | "name": "context", 192 | "description": "The context to continue to.", 193 | "paramType": "query", 194 | "required": false, 195 | "allowMultiple": false, 196 | "dataType": "string" 197 | }, 198 | { 199 | "name": "extension", 200 | "description": "The extension to continue to.", 201 | "paramType": "query", 202 | "required": false, 203 | "allowMultiple": false, 204 | "dataType": "string" 205 | }, 206 | { 207 | "name": "priority", 208 | "description": "The priority to continue to.", 209 | "paramType": "query", 210 | "required": false, 211 | "allowMultiple": false, 212 | "dataType": "int" 213 | } 214 | ], 215 | "errorResponses": [ 216 | { 217 | "code": 404, 218 | "reason": "Channel not found" 219 | }, 220 | { 221 | "code": 409, 222 | "reason": "Channel not in a Stasis application" 223 | } 224 | ] 225 | } 226 | ] 227 | }, 228 | { 229 | "path": "/channels/{channelId}/answer", 230 | "description": "Answer a channel", 231 | "operations": [ 232 | { 233 | "httpMethod": "POST", 234 | "summary": "Answer a channel.", 235 | "nickname": "answer", 236 | "responseClass": "void", 237 | "parameters": [ 238 | { 239 | "name": "channelId", 240 | "description": "Channel's id", 241 | "paramType": "path", 242 | "required": true, 243 | "allowMultiple": false, 244 | "dataType": "string" 245 | } 246 | ], 247 | "errorResponses": [ 248 | { 249 | "code": 404, 250 | "reason": "Channel not found" 251 | }, 252 | { 253 | "code": 409, 254 | "reason": "Channel not in a Stasis application" 255 | } 256 | ] 257 | } 258 | ] 259 | }, 260 | { 261 | "path": "/channels/{channelId}/ring", 262 | "description": "Send a ringing indication to a channel", 263 | "operations": [ 264 | { 265 | "httpMethod": "POST", 266 | "summary": "Indicate ringing to a channel.", 267 | "nickname": "ring", 268 | "responseClass": "void", 269 | "parameters": [ 270 | { 271 | "name": "channelId", 272 | "description": "Channel's id", 273 | "paramType": "path", 274 | "required": true, 275 | "allowMultiple": false, 276 | "dataType": "string" 277 | } 278 | ], 279 | "errorResponses": [ 280 | { 281 | "code": 404, 282 | "reason": "Channel not found" 283 | }, 284 | { 285 | "code": 409, 286 | "reason": "Channel not in a Stasis application" 287 | } 288 | ] 289 | }, 290 | { 291 | "httpMethod": "DELETE", 292 | "summary": "Stop ringing indication on a channel if locally generated.", 293 | "nickname": "ringStop", 294 | "responseClass": "void", 295 | "parameters": [ 296 | { 297 | "name": "channelId", 298 | "description": "Channel's id", 299 | "paramType": "path", 300 | "required": true, 301 | "allowMultiple": false, 302 | "dataType": "string" 303 | } 304 | ], 305 | "errorResponses": [ 306 | { 307 | "code": 404, 308 | "reason": "Channel not found" 309 | }, 310 | { 311 | "code": 409, 312 | "reason": "Channel not in a Stasis application" 313 | } 314 | ] 315 | } 316 | ] 317 | }, 318 | { 319 | "path": "/channels/{channelId}/dtmf", 320 | "description": "Send DTMF to a channel", 321 | "operations": [ 322 | { 323 | "httpMethod": "POST", 324 | "summary": "Send provided DTMF to a given channel.", 325 | "nickname": "sendDTMF", 326 | "responseClass": "void", 327 | "parameters": [ 328 | { 329 | "name": "channelId", 330 | "description": "Channel's id", 331 | "paramType": "path", 332 | "required": true, 333 | "allowMultiple": false, 334 | "dataType": "string" 335 | }, 336 | { 337 | "name": "dtmf", 338 | "description": "DTMF To send.", 339 | "paramType": "query", 340 | "required": false, 341 | "allowMultiple": false, 342 | "dataType": "string" 343 | }, 344 | { 345 | "name": "before", 346 | "description": "Amount of time to wait before DTMF digits (specified in milliseconds) start.", 347 | "paramType": "query", 348 | "required": false, 349 | "allowMultiple": false, 350 | "dataType": "int", 351 | "defaultValue": 0 352 | }, 353 | { 354 | "name": "between", 355 | "description": "Amount of time in between DTMF digits (specified in milliseconds).", 356 | "paramType": "query", 357 | "required": false, 358 | "allowMultiple": false, 359 | "dataType": "int", 360 | "defaultValue": 100 361 | }, 362 | { 363 | "name": "duration", 364 | "description": "Length of each DTMF digit (specified in milliseconds).", 365 | "paramType": "query", 366 | "required": false, 367 | "allowMultiple": false, 368 | "dataType": "int", 369 | "defaultValue": 100 370 | }, 371 | { 372 | "name": "after", 373 | "description": "Amount of time to wait after DTMF digits (specified in milliseconds) end.", 374 | "paramType": "query", 375 | "required": false, 376 | "allowMultiple": false, 377 | "dataType": "int", 378 | "defaultValue": 0 379 | } 380 | ], 381 | "errorResponses": [ 382 | { 383 | "code": 400, 384 | "reason": "DTMF is required" 385 | }, 386 | { 387 | "code": 404, 388 | "reason": "Channel not found" 389 | }, 390 | { 391 | "code": 409, 392 | "reason": "Channel not in a Stasis application" 393 | } 394 | ] 395 | } 396 | ] 397 | }, 398 | { 399 | "path": "/channels/{channelId}/mute", 400 | "description": "Mute a channel", 401 | "operations": [ 402 | { 403 | "httpMethod": "POST", 404 | "summary": "Mute a channel.", 405 | "nickname": "mute", 406 | "responseClass": "void", 407 | "parameters": [ 408 | { 409 | "name": "channelId", 410 | "description": "Channel's id", 411 | "paramType": "path", 412 | "required": true, 413 | "allowMultiple": false, 414 | "dataType": "string" 415 | }, 416 | { 417 | "name": "direction", 418 | "description": "Direction in which to mute audio", 419 | "paramType": "query", 420 | "required": false, 421 | "allowMultiple": false, 422 | "dataType": "string", 423 | "defaultValue": "both", 424 | "allowableValues": { 425 | "valueType": "LIST", 426 | "values": [ 427 | "both", 428 | "in", 429 | "out" 430 | ] 431 | } 432 | } 433 | ], 434 | "errorResponses": [ 435 | { 436 | "code": 404, 437 | "reason": "Channel not found" 438 | }, 439 | { 440 | "code": 409, 441 | "reason": "Channel not in a Stasis application" 442 | } 443 | ] 444 | }, 445 | { 446 | "httpMethod": "DELETE", 447 | "summary": "Unmute a channel.", 448 | "nickname": "unmute", 449 | "responseClass": "void", 450 | "parameters": [ 451 | { 452 | "name": "channelId", 453 | "description": "Channel's id", 454 | "paramType": "path", 455 | "required": true, 456 | "allowMultiple": false, 457 | "dataType": "string" 458 | }, 459 | { 460 | "name": "direction", 461 | "description": "Direction in which to unmute audio", 462 | "paramType": "query", 463 | "required": false, 464 | "allowMultiple": false, 465 | "dataType": "string", 466 | "defaultValue": "both", 467 | "allowableValues": { 468 | "valueType": "LIST", 469 | "values": [ 470 | "both", 471 | "in", 472 | "out" 473 | ] 474 | } 475 | } 476 | ], 477 | "errorResponses": [ 478 | { 479 | "code": 404, 480 | "reason": "Channel not found" 481 | }, 482 | { 483 | "code": 409, 484 | "reason": "Channel not in a Stasis application" 485 | } 486 | ] 487 | } 488 | ] 489 | }, 490 | { 491 | "path": "/channels/{channelId}/hold", 492 | "description": "Put a channel on hold", 493 | "operations": [ 494 | { 495 | "httpMethod": "POST", 496 | "summary": "Hold a channel.", 497 | "nickname": "hold", 498 | "responseClass": "void", 499 | "parameters": [ 500 | { 501 | "name": "channelId", 502 | "description": "Channel's id", 503 | "paramType": "path", 504 | "required": true, 505 | "allowMultiple": false, 506 | "dataType": "string" 507 | } 508 | ], 509 | "errorResponses": [ 510 | { 511 | "code": 404, 512 | "reason": "Channel not found" 513 | }, 514 | { 515 | "code": 409, 516 | "reason": "Channel not in a Stasis application" 517 | } 518 | ] 519 | }, 520 | { 521 | "httpMethod": "DELETE", 522 | "summary": "Remove a channel from hold.", 523 | "nickname": "unhold", 524 | "responseClass": "void", 525 | "parameters": [ 526 | { 527 | "name": "channelId", 528 | "description": "Channel's id", 529 | "paramType": "path", 530 | "required": true, 531 | "allowMultiple": false, 532 | "dataType": "string" 533 | } 534 | ], 535 | "errorResponses": [ 536 | { 537 | "code": 404, 538 | "reason": "Channel not found" 539 | }, 540 | { 541 | "code": 409, 542 | "reason": "Channel not in a Stasis application" 543 | } 544 | ] 545 | } 546 | ] 547 | }, 548 | { 549 | "path": "/channels/{channelId}/moh", 550 | "description": "Play music on hold to a channel", 551 | "operations": [ 552 | { 553 | "httpMethod": "POST", 554 | "summary": "Play music on hold to a channel.", 555 | "notes": "Using media operations such as playOnChannel on a channel playing MOH in this manner will suspend MOH without resuming automatically. If continuing music on hold is desired, the stasis application must reinitiate music on hold.", 556 | "nickname": "startMoh", 557 | "responseClass": "void", 558 | "parameters": [ 559 | { 560 | "name": "channelId", 561 | "description": "Channel's id", 562 | "paramType": "path", 563 | "required": true, 564 | "allowMultiple": false, 565 | "dataType": "string" 566 | }, 567 | { 568 | "name": "mohClass", 569 | "description": "Music on hold class to use", 570 | "paramType": "query", 571 | "required": false, 572 | "allowMultiple": false, 573 | "dataType": "string" 574 | } 575 | ], 576 | "errorResponses": [ 577 | { 578 | "code": 404, 579 | "reason": "Channel not found" 580 | }, 581 | { 582 | "code": 409, 583 | "reason": "Channel not in a Stasis application" 584 | } 585 | ] 586 | }, 587 | { 588 | "httpMethod": "DELETE", 589 | "summary": "Stop playing music on hold to a channel.", 590 | "nickname": "stopMoh", 591 | "responseClass": "void", 592 | "parameters": [ 593 | { 594 | "name": "channelId", 595 | "description": "Channel's id", 596 | "paramType": "path", 597 | "required": true, 598 | "allowMultiple": false, 599 | "dataType": "string" 600 | } 601 | ], 602 | "errorResponses": [ 603 | { 604 | "code": 404, 605 | "reason": "Channel not found" 606 | }, 607 | { 608 | "code": 409, 609 | "reason": "Channel not in a Stasis application" 610 | } 611 | ] 612 | } 613 | ] 614 | }, 615 | { 616 | "path": "/channels/{channelId}/play", 617 | "description": "Play media to a channel", 618 | "operations": [ 619 | { 620 | "httpMethod": "POST", 621 | "summary": "Start playback of media.", 622 | "notes": "The media URI may be any of a number of URI's. Currently sound: and recording: URI's are supported. This operation creates a playback resource that can be used to control the playback of media (pause, rewind, fast forward, etc.)", 623 | "nickname": "play", 624 | "responseClass": "Playback", 625 | "parameters": [ 626 | { 627 | "name": "channelId", 628 | "description": "Channel's id", 629 | "paramType": "path", 630 | "required": true, 631 | "allowMultiple": false, 632 | "dataType": "string" 633 | }, 634 | { 635 | "name": "media", 636 | "description": "Media's URI to play.", 637 | "paramType": "query", 638 | "required": true, 639 | "allowMultiple": false, 640 | "dataType": "string" 641 | }, 642 | { 643 | "name": "lang", 644 | "description": "For sounds, selects language for sound.", 645 | "paramType": "query", 646 | "required": false, 647 | "allowMultiple": false, 648 | "dataType": "string" 649 | }, 650 | { 651 | "name": "offsetms", 652 | "description": "Number of media to skip before playing.", 653 | "paramType": "query", 654 | "required": false, 655 | "allowMultiple": false, 656 | "dataType": "int" 657 | }, 658 | { 659 | "name": "skipms", 660 | "description": "Number of milliseconds to skip for forward/reverse operations.", 661 | "paramType": "query", 662 | "required": false, 663 | "allowMultiple": false, 664 | "dataType": "int", 665 | "defaultValue": 3000 666 | } 667 | ], 668 | "errorResponses": [ 669 | { 670 | "code": 404, 671 | "reason": "Channel not found" 672 | }, 673 | { 674 | "code": 409, 675 | "reason": "Channel not in a Stasis application" 676 | } 677 | ] 678 | } 679 | ] 680 | }, 681 | { 682 | "path": "/channels/{channelId}/record", 683 | "description": "Record audio from a channel", 684 | "operations": [ 685 | { 686 | "httpMethod": "POST", 687 | "summary": "Start a recording.", 688 | "notes": "Record audio from a channel. Note that this will not capture audio sent to the channel. The bridge itself has a record feature if that's what you want.", 689 | "nickname": "record", 690 | "responseClass": "LiveRecording", 691 | "parameters": [ 692 | { 693 | "name": "channelId", 694 | "description": "Channel's id", 695 | "paramType": "path", 696 | "required": true, 697 | "allowMultiple": false, 698 | "dataType": "string" 699 | }, 700 | { 701 | "name": "name", 702 | "description": "Recording's filename", 703 | "paramType": "query", 704 | "required": true, 705 | "allowMultiple": false, 706 | "dataType": "string" 707 | }, 708 | { 709 | "name": "format", 710 | "description": "Format to encode audio in", 711 | "paramType": "query", 712 | "required": true, 713 | "allowMultiple": false, 714 | "dataType": "string" 715 | }, 716 | { 717 | "name": "maxDurationSeconds", 718 | "description": "Maximum duration of the recording, in seconds. 0 for no limit", 719 | "paramType": "query", 720 | "required": false, 721 | "allowMultiple": false, 722 | "dataType": "int", 723 | "defaultValue": 0, 724 | "allowableValues": { 725 | "valueType": "RANGE", 726 | "min": 0 727 | } 728 | }, 729 | { 730 | "name": "maxSilenceSeconds", 731 | "description": "Maximum duration of silence, in seconds. 0 for no limit", 732 | "paramType": "query", 733 | "required": false, 734 | "allowMultiple": false, 735 | "dataType": "int", 736 | "defaultValue": 0, 737 | "allowableValues": { 738 | "valueType": "RANGE", 739 | "min": 0 740 | } 741 | }, 742 | { 743 | "name": "ifExists", 744 | "description": "Action to take if a recording with the same name already exists.", 745 | "paramType": "query", 746 | "required": false, 747 | "allowMultiple": false, 748 | "dataType": "string", 749 | "defaultValue": "fail", 750 | "allowableValues": { 751 | "valueType": "LIST", 752 | "values": [ 753 | "fail", 754 | "overwrite", 755 | "append" 756 | ] 757 | } 758 | }, 759 | { 760 | "name": "beep", 761 | "description": "Play beep when recording begins", 762 | "paramType": "query", 763 | "required": false, 764 | "allowMultiple": false, 765 | "dataType": "boolean", 766 | "defaultValue": false 767 | }, 768 | { 769 | "name": "terminateOn", 770 | "description": "DTMF input to terminate recording", 771 | "paramType": "query", 772 | "required": false, 773 | "allowMultiple": false, 774 | "dataType": "string", 775 | "defaultValue": "none", 776 | "allowableValues": { 777 | "valueType": "LIST", 778 | "values": [ 779 | "none", 780 | "any", 781 | "*", 782 | "#" 783 | ] 784 | } 785 | } 786 | ], 787 | "errorResponses": [ 788 | { 789 | "code": 400, 790 | "reason": "Invalid parameters" 791 | }, 792 | { 793 | "code": 404, 794 | "reason": "Channel not found" 795 | }, 796 | { 797 | "code": 409, 798 | "reason": "Channel is not in a Stasis application; the channel is currently bridged with other hcannels; A recording with the same name already exists on the system and can not be overwritten because it is in progress or ifExists=fail" 799 | }, 800 | { 801 | "code": 422, 802 | "reason": "The format specified is unknown on this system" 803 | } 804 | ] 805 | } 806 | ] 807 | }, 808 | { 809 | "path": "/channels/{channelId}/variable", 810 | "description": "Variables on a channel", 811 | "operations": [ 812 | { 813 | "httpMethod": "GET", 814 | "summary": "Get the value of a channel variable or function.", 815 | "nickname": "getChannelVar", 816 | "responseClass": "Variable", 817 | "parameters": [ 818 | { 819 | "name": "channelId", 820 | "description": "Channel's id", 821 | "paramType": "path", 822 | "required": true, 823 | "allowMultiple": false, 824 | "dataType": "string" 825 | }, 826 | { 827 | "name": "variable", 828 | "description": "The channel variable or function to get", 829 | "paramType": "query", 830 | "required": true, 831 | "allowMultiple": false, 832 | "dataType": "string" 833 | } 834 | ], 835 | "errorResponses": [ 836 | { 837 | "code": 400, 838 | "reason": "Missing variable parameter." 839 | }, 840 | { 841 | "code": 404, 842 | "reason": "Channel not found" 843 | }, 844 | { 845 | "code": 409, 846 | "reason": "Channel not in a Stasis application" 847 | } 848 | ] 849 | }, 850 | { 851 | "httpMethod": "POST", 852 | "summary": "Set the value of a channel variable or function.", 853 | "nickname": "setChannelVar", 854 | "responseClass": "void", 855 | "parameters": [ 856 | { 857 | "name": "channelId", 858 | "description": "Channel's id", 859 | "paramType": "path", 860 | "required": true, 861 | "allowMultiple": false, 862 | "dataType": "string" 863 | }, 864 | { 865 | "name": "variable", 866 | "description": "The channel variable or function to set", 867 | "paramType": "query", 868 | "required": true, 869 | "allowMultiple": false, 870 | "dataType": "string" 871 | }, 872 | { 873 | "name": "value", 874 | "description": "The value to set the variable to", 875 | "paramType": "query", 876 | "required": false, 877 | "allowMultiple": false, 878 | "dataType": "string" 879 | } 880 | ], 881 | "errorResponses": [ 882 | { 883 | "code": 400, 884 | "reason": "Missing variable parameter." 885 | }, 886 | { 887 | "code": 404, 888 | "reason": "Channel not found" 889 | }, 890 | { 891 | "code": 409, 892 | "reason": "Channel not in a Stasis application" 893 | } 894 | ] 895 | } 896 | ] 897 | } 898 | ], 899 | "models": { 900 | "Dialed": { 901 | "id": "Dialed", 902 | "description": "Dialed channel information.", 903 | "properties": {} 904 | }, 905 | "DialplanCEP": { 906 | "id": "DialplanCEP", 907 | "description": "Dialplan location (context/extension/priority)", 908 | "properties": { 909 | "context": { 910 | "required": true, 911 | "type": "string", 912 | "description": "Context in the dialplan" 913 | }, 914 | "exten": { 915 | "required": true, 916 | "type": "string", 917 | "description": "Extension in the dialplan" 918 | }, 919 | "priority": { 920 | "required": true, 921 | "type": "long", 922 | "description": "Priority in the dialplan" 923 | } 924 | } 925 | }, 926 | "CallerID": { 927 | "id": "CallerID", 928 | "description": "Caller identification", 929 | "properties": { 930 | "name": { 931 | "required": true, 932 | "type": "string" 933 | }, 934 | "number": { 935 | "required": true, 936 | "type": "string" 937 | } 938 | } 939 | }, 940 | "Channel": { 941 | "id": "Channel", 942 | "description": "A specific communication connection between Asterisk and an Endpoint.", 943 | "properties": { 944 | "id": { 945 | "required": true, 946 | "type": "string", 947 | "description": "Unique identifier of the channel.\n\nThis is the same as the Uniqueid field in AMI." 948 | }, 949 | "name": { 950 | "required": true, 951 | "type": "string", 952 | "description": "Name of the channel (i.e. SIP/foo-0000a7e3)" 953 | }, 954 | "state": { 955 | "required": true, 956 | "type": "string", 957 | "allowableValues": { 958 | "valueType": "LIST", 959 | "values": [ 960 | "Down", 961 | "Rsrved", 962 | "OffHook", 963 | "Dialing", 964 | "Ring", 965 | "Ringing", 966 | "Up", 967 | "Busy", 968 | "Dialing Offhook", 969 | "Pre-ring", 970 | "Unknown" 971 | ] 972 | } 973 | }, 974 | "caller": { 975 | "required": true, 976 | "type": "CallerID" 977 | }, 978 | "connected": { 979 | "required": true, 980 | "type": "CallerID" 981 | }, 982 | "accountcode": { 983 | "required": true, 984 | "type": "string" 985 | }, 986 | "dialplan": { 987 | "required": true, 988 | "type": "DialplanCEP", 989 | "description": "Current location in the dialplan" 990 | }, 991 | "creationtime": { 992 | "required": true, 993 | "type": "Date", 994 | "description": "Timestamp when channel was created" 995 | } 996 | } 997 | } 998 | } 999 | } 1000 | -------------------------------------------------------------------------------- /sample-api/deviceStates.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "Kevin Harwell ", 4 | "_svn_revision": "$Revision: 407402 $", 5 | "apiVersion": "0.0.0-test", 6 | "swaggerVersion": "1.1", 7 | "basePath": "http://ari.py/ari", 8 | "resourcePath": "/api-docs/deviceStates.{format}", 9 | "apis": [ 10 | { 11 | "path": "/deviceStates", 12 | "description": "Device states", 13 | "operations": [ 14 | { 15 | "httpMethod": "GET", 16 | "summary": "List all ARI controlled device states.", 17 | "nickname": "list", 18 | "responseClass": "List[DeviceState]" 19 | } 20 | ] 21 | }, 22 | { 23 | "path": "/deviceStates/{deviceName}", 24 | "description": "Device state", 25 | "operations": [ 26 | { 27 | "httpMethod": "GET", 28 | "summary": "Retrieve the current state of a device.", 29 | "nickname": "get", 30 | "responseClass": "DeviceState", 31 | "parameters": [ 32 | { 33 | "name": "deviceName", 34 | "description": "Name of the device", 35 | "paramType": "path", 36 | "required": true, 37 | "allowMultiple": false, 38 | "dataType": "string" 39 | } 40 | ] 41 | }, 42 | { 43 | "httpMethod": "PUT", 44 | "summary": "Change the state of a device controlled by ARI. (Note - implicitly creates the device state).", 45 | "nickname": "update", 46 | "responseClass": "void", 47 | "parameters": [ 48 | { 49 | "name": "deviceName", 50 | "description": "Name of the device", 51 | "paramType": "path", 52 | "required": true, 53 | "allowMultiple": false, 54 | "dataType": "string" 55 | }, 56 | { 57 | "name": "deviceState", 58 | "description": "Device state value", 59 | "paramType": "query", 60 | "required": true, 61 | "allowMultiple": false, 62 | "dataType": "string", 63 | "allowableValues": { 64 | "valueType": "LIST", 65 | "values": [ 66 | "NOT_INUSE", 67 | "INUSE", 68 | "BUSY", 69 | "INVALID", 70 | "UNAVAILABLE", 71 | "RINGING", 72 | "RINGINUSE", 73 | "ONHOLD" 74 | ] 75 | } 76 | 77 | } 78 | ], 79 | "errorResponses": [ 80 | { 81 | "code": 404, 82 | "reason": "Device name is missing" 83 | }, 84 | { 85 | "code": 409, 86 | "reason": "Uncontrolled device specified" 87 | } 88 | ] 89 | }, 90 | { 91 | "httpMethod": "DELETE", 92 | "summary": "Destroy a device-state controlled by ARI.", 93 | "nickname": "delete", 94 | "responseClass": "void", 95 | "parameters": [ 96 | { 97 | "name": "deviceName", 98 | "description": "Name of the device", 99 | "paramType": "path", 100 | "required": true, 101 | "allowMultiple": false, 102 | "dataType": "string" 103 | } 104 | ], 105 | "errorResponses": [ 106 | { 107 | "code": 404, 108 | "reason": "Device name is missing" 109 | }, 110 | { 111 | "code": 409, 112 | "reason": "Uncontrolled device specified" 113 | } 114 | ] 115 | } 116 | ] 117 | } 118 | ], 119 | "models": { 120 | "DeviceState": { 121 | "id": "DeviceState", 122 | "description": "Represents the state of a device.", 123 | "properties": { 124 | "name": { 125 | "type": "string", 126 | "description": "Name of the device.", 127 | "required": true 128 | }, 129 | "state": { 130 | "type": "string", 131 | "description": "Device's state", 132 | "required": true, 133 | "allowableValues": { 134 | "valueType": "LIST", 135 | "values": [ 136 | "UNKNOWN", 137 | "NOT_INUSE", 138 | "INUSE", 139 | "BUSY", 140 | "INVALID", 141 | "UNAVAILABLE", 142 | "RINGING", 143 | "RINGINUSE", 144 | "ONHOLD" 145 | ] 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /sample-api/endpoints.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/endpoints.{format}", 8 | "apis": [ 9 | { 10 | "path": "/endpoints", 11 | "description": "Asterisk endpoints", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "summary": "List all endpoints.", 16 | "nickname": "list", 17 | "responseClass": "List[Endpoint]" 18 | } 19 | ] 20 | }, 21 | { 22 | "path": "/endpoints/{tech}", 23 | "description": "Asterisk endpoints", 24 | "operations": [ 25 | { 26 | "httpMethod": "GET", 27 | "summary": "List available endoints for a given endpoint technology.", 28 | "nickname": "listByTech", 29 | "responseClass": "List[Endpoint]", 30 | "parameters": [ 31 | { 32 | "name": "tech", 33 | "description": "Technology of the endpoints (sip,iax2,...)", 34 | "paramType": "path", 35 | "dataType": "string" 36 | } 37 | ], 38 | "errorResponses": [ 39 | { 40 | "code": 404, 41 | "reason": "Endpoints not found" 42 | } 43 | ] 44 | } 45 | ] 46 | }, 47 | { 48 | "path": "/endpoints/{tech}/{resource}", 49 | "description": "Single endpoint", 50 | "operations": [ 51 | { 52 | "httpMethod": "GET", 53 | "summary": "Details for an endpoint.", 54 | "nickname": "get", 55 | "responseClass": "Endpoint", 56 | "parameters": [ 57 | { 58 | "name": "tech", 59 | "description": "Technology of the endpoint", 60 | "paramType": "path", 61 | "dataType": "string" 62 | }, 63 | { 64 | "name": "resource", 65 | "description": "ID of the endpoint", 66 | "paramType": "path", 67 | "dataType": "string" 68 | } 69 | ], 70 | "errorResponses": [ 71 | { 72 | "code": 404, 73 | "reason": "Endpoints not found" 74 | } 75 | ] 76 | } 77 | ] 78 | } 79 | ], 80 | "models": { 81 | "Endpoint": { 82 | "id": "Endpoint", 83 | "description": "An external device that may offer/accept calls to/from Asterisk.\n\nUnlike most resources, which have a single unique identifier, an endpoint is uniquely identified by the technology/resource pair.", 84 | "properties": { 85 | "technology": { 86 | "type": "string", 87 | "description": "Technology of the endpoint", 88 | "required": true 89 | }, 90 | "resource": { 91 | "type": "string", 92 | "description": "Identifier of the endpoint, specific to the given technology.", 93 | "required": true 94 | }, 95 | "state": { 96 | "type": "string", 97 | "description": "Endpoint's state", 98 | "required": false, 99 | "allowableValues": { 100 | "valueType": "LIST", 101 | "values": [ 102 | "unknown", 103 | "offline", 104 | "online" 105 | ] 106 | } 107 | }, 108 | "channel_ids": { 109 | "type": "List[string]", 110 | "description": "Id's of channels associated with this endpoint", 111 | "required": true 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sample-api/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.2", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/events.{format}", 8 | "apis": [ 9 | { 10 | "path": "/events", 11 | "description": "Events from Asterisk to applications", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "upgrade": "websocket", 16 | "websocketProtocol": "ari", 17 | "summary": "WebSocket connection for events.", 18 | "nickname": "eventWebsocket", 19 | "responseClass": "Message", 20 | "parameters": [ 21 | { 22 | "name": "app", 23 | "description": "Applications to subscribe to.", 24 | "paramType": "query", 25 | "required": true, 26 | "allowMultiple": true, 27 | "dataType": "string" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | ], 34 | "models": { 35 | "Message": { 36 | "id": "Message", 37 | "description": "Base type for errors and events", 38 | "discriminator": "type", 39 | "properties": { 40 | "type": { 41 | "type": "string", 42 | "required": true, 43 | "description": "Indicates the type of this message." 44 | } 45 | }, 46 | "subTypes": [ 47 | "MissingParams", 48 | "Event" 49 | ] 50 | }, 51 | "MissingParams": { 52 | "id": "MissingParams", 53 | "description": "Error event sent when required params are missing.", 54 | "properties": { 55 | "params": { 56 | "required": true, 57 | "type": "List[string]", 58 | "description": "A list of the missing parameters" 59 | } 60 | } 61 | }, 62 | "Event": { 63 | "id": "Event", 64 | "description": "Base type for asynchronous events from Asterisk.", 65 | "properties": { 66 | "application": { 67 | "type": "string", 68 | "description": "Name of the application receiving the event.", 69 | "required": true 70 | }, 71 | "timestamp": { 72 | "type": "Date", 73 | "description": "Time at which this event was created.", 74 | "required": false 75 | } 76 | }, 77 | "subTypes": [ 78 | "PlaybackStarted", 79 | "PlaybackFinished", 80 | "ApplicationReplaced", 81 | "BridgeCreated", 82 | "BridgeDestroyed", 83 | "BridgeMerged", 84 | "ChannelCreated", 85 | "ChannelDestroyed", 86 | "ChannelEnteredBridge", 87 | "ChannelLeftBridge", 88 | "ChannelStateChange", 89 | "ChannelDtmfReceived", 90 | "ChannelDialplan", 91 | "ChannelCallerId", 92 | "ChannelUserevent", 93 | "ChannelHangupRequest", 94 | "ChannelVarset", 95 | "EndpointStateChange", 96 | "StasisEnd", 97 | "StasisStart" 98 | ] 99 | }, 100 | "PlaybackStarted": { 101 | "id": "PlaybackStarted", 102 | "description": "Event showing the start of a media playback operation.", 103 | "properties": { 104 | "playback": { 105 | "type": "Playback", 106 | "description": "Playback control object", 107 | "required": true 108 | } 109 | } 110 | }, 111 | "PlaybackFinished": { 112 | "id": "PlaybackFinished", 113 | "description": "Event showing the completion of a media playback operation.", 114 | "properties": { 115 | "playback": { 116 | "type": "Playback", 117 | "description": "Playback control object", 118 | "required": true 119 | } 120 | } 121 | }, 122 | "RecordingStarted": { 123 | "id": "RecordingStarted", 124 | "extends": "Event", 125 | "description": "Event showing the start of a recording operation.", 126 | "properties": { 127 | "recording": { 128 | "type": "LiveRecording", 129 | "description": "Recording control object", 130 | "required": true 131 | } 132 | } 133 | }, 134 | "RecordingFinished": { 135 | "id": "RecordingFinished", 136 | "extends": "Event", 137 | "description": "Event showing the completion of a recording operation.", 138 | "properties": { 139 | "recording": { 140 | "type": "LiveRecording", 141 | "description": "Recording control object", 142 | "required": true 143 | } 144 | } 145 | }, 146 | "RecordingFailed": { 147 | "id": "RecordingFailed", 148 | "extends": "Event", 149 | "description": "Event showing failure of a recording operation.", 150 | "properties": { 151 | "recording": { 152 | "type": "LiveRecording", 153 | "description": "Recording control object", 154 | "required": true 155 | }, 156 | "cause": { 157 | "type": "string", 158 | "description": "Cause for the recording failure", 159 | "required": true 160 | } 161 | } 162 | }, 163 | "ApplicationReplaced": { 164 | "id": "ApplicationReplaced", 165 | "description": "Notification that another WebSocket has taken over for an application.\n\nAn application may only be subscribed to by a single WebSocket at a time. If multiple WebSockets attempt to subscribe to the same application, the newer WebSocket wins, and the older one receives this event.", 166 | "properties": {} 167 | }, 168 | "BridgeCreated": { 169 | "id": "BridgeCreated", 170 | "description": "Notification that a bridge has been created.", 171 | "properties": { 172 | "bridge": { 173 | "required": true, 174 | "type": "Bridge" 175 | } 176 | } 177 | }, 178 | "BridgeDestroyed": { 179 | "id": "BridgeDestroyed", 180 | "description": "Notification that a bridge has been destroyed.", 181 | "properties": { 182 | "bridge": { 183 | "required": true, 184 | "type": "Bridge" 185 | } 186 | } 187 | }, 188 | "BridgeMerged": { 189 | "id": "BridgeMerged", 190 | "description": "Notification that one bridge has merged into another.", 191 | "properties": { 192 | "bridge": { 193 | "required": true, 194 | "type": "Bridge" 195 | }, 196 | "bridge_from": { 197 | "required": true, 198 | "type": "Bridge" 199 | } 200 | } 201 | }, 202 | "ChannelCreated": { 203 | "id": "ChannelCreated", 204 | "description": "Notification that a channel has been created.", 205 | "properties": { 206 | "channel": { 207 | "required": true, 208 | "type": "Channel" 209 | } 210 | } 211 | }, 212 | "ChannelDestroyed": { 213 | "id": "ChannelDestroyed", 214 | "description": "Notification that a channel has been destroyed.", 215 | "properties": { 216 | "cause": { 217 | "required": true, 218 | "description": "Integer representation of the cause of the hangup", 219 | "type": "int" 220 | }, 221 | "cause_txt": { 222 | "required": true, 223 | "description": "Text representation of the cause of the hangup", 224 | "type": "string" 225 | }, 226 | "channel": { 227 | "required": true, 228 | "type": "Channel" 229 | } 230 | } 231 | }, 232 | "ChannelEnteredBridge": { 233 | "id": "ChannelEnteredBridge", 234 | "description": "Notification that a channel has entered a bridge.", 235 | "properties": { 236 | "bridge": { 237 | "required": true, 238 | "type": "Bridge" 239 | }, 240 | "channel": { 241 | "type": "Channel" 242 | } 243 | } 244 | }, 245 | "ChannelLeftBridge": { 246 | "id": "ChannelLeftBridge", 247 | "description": "Notification that a channel has left a bridge.", 248 | "properties": { 249 | "bridge": { 250 | "required": true, 251 | "type": "Bridge" 252 | }, 253 | "channel": { 254 | "required": true, 255 | "type": "Channel" 256 | } 257 | } 258 | }, 259 | "ChannelStateChange": { 260 | "id": "ChannelStateChange", 261 | "description": "Notification of a channel's state change.", 262 | "properties": { 263 | "channel": { 264 | "required": true, 265 | "type": "Channel" 266 | } 267 | } 268 | }, 269 | "ChannelDtmfReceived": { 270 | "id": "ChannelDtmfReceived", 271 | "description": "DTMF received on a channel.\n\nThis event is sent when the DTMF ends. There is no notification about the start of DTMF", 272 | "properties": { 273 | "digit": { 274 | "required": true, 275 | "type": "string", 276 | "description": "DTMF digit received (0-9, A-E, # or *)" 277 | }, 278 | "duration_ms": { 279 | "required": true, 280 | "type": "int", 281 | "description": "Number of milliseconds DTMF was received" 282 | }, 283 | "channel": { 284 | "required": true, 285 | "type": "Channel", 286 | "description": "The channel on which DTMF was received" 287 | } 288 | } 289 | }, 290 | "ChannelDialplan": { 291 | "id": "ChannelDialplan", 292 | "description": "Channel changed location in the dialplan.", 293 | "properties": { 294 | "channel": { 295 | "required": true, 296 | "type": "Channel", 297 | "description": "The channel that changed dialplan location." 298 | }, 299 | "dialplan_app": { 300 | "required": true, 301 | "type": "string", 302 | "description": "The application about to be executed." 303 | }, 304 | "dialplan_app_data": { 305 | "required": true, 306 | "type": "string", 307 | "description": "The data to be passed to the application." 308 | } 309 | } 310 | }, 311 | "ChannelCallerId": { 312 | "id": "ChannelCallerId", 313 | "description": "Channel changed Caller ID.", 314 | "properties": { 315 | "caller_presentation": { 316 | "required": true, 317 | "type": "int", 318 | "description": "The integer representation of the Caller Presentation value." 319 | }, 320 | "caller_presentation_txt": { 321 | "required": true, 322 | "type": "string", 323 | "description": "The text representation of the Caller Presentation value." 324 | }, 325 | "channel": { 326 | "required": true, 327 | "type": "Channel", 328 | "description": "The channel that changed Caller ID." 329 | } 330 | } 331 | }, 332 | "ChannelUserevent": { 333 | "id": "ChannelUserevent", 334 | "description": "User-generated event with additional user-defined fields in the object.", 335 | "properties": { 336 | "eventname": { 337 | "required": true, 338 | "type": "string", 339 | "description": "The name of the user event." 340 | }, 341 | "channel": { 342 | "required": true, 343 | "type": "Channel", 344 | "description": "The channel that signaled the user event." 345 | }, 346 | "userevent": { 347 | "required": true, 348 | "type": "object", 349 | "description": "Custom Userevent data" 350 | } 351 | } 352 | }, 353 | "ChannelHangupRequest": { 354 | "id": "ChannelHangupRequest", 355 | "description": "A hangup was requested on the channel.", 356 | "properties": { 357 | "cause": { 358 | "type": "int", 359 | "description": "Integer representation of the cause of the hangup." 360 | }, 361 | "soft": { 362 | "type": "boolean", 363 | "description": "Whether the hangup request was a soft hangup request." 364 | }, 365 | "channel": { 366 | "required": true, 367 | "type": "Channel", 368 | "description": "The channel on which the hangup was requested." 369 | } 370 | } 371 | }, 372 | "ChannelVarset": { 373 | "id": "ChannelVarset", 374 | "description": "Channel variable changed.", 375 | "properties": { 376 | "variable": { 377 | "required": true, 378 | "type": "string", 379 | "description": "The variable that changed." 380 | }, 381 | "value": { 382 | "required": true, 383 | "type": "string", 384 | "description": "The new value of the variable." 385 | }, 386 | "channel": { 387 | "required": false, 388 | "type": "Channel", 389 | "description": "The channel on which the variable was set.\n\nIf missing, the variable is a global variable." 390 | } 391 | } 392 | }, 393 | "EndpointStateChange": { 394 | "id": "EndpointStateChange", 395 | "description": "Endpoint state changed.", 396 | "properties": { 397 | "endpoint": { 398 | "required": true, 399 | "type": "Endpoint" 400 | } 401 | } 402 | }, 403 | "StasisEnd": { 404 | "id": "StasisEnd", 405 | "description": "Notification that a channel has left a Stasis application.", 406 | "properties": { 407 | "channel": { 408 | "required": true, 409 | "type": "Channel" 410 | } 411 | } 412 | }, 413 | "StasisStart": { 414 | "id": "StasisStart", 415 | "description": "Notification that a channel has entered a Stasis application.", 416 | "properties": { 417 | "args": { 418 | "required": true, 419 | "type": "List[string]", 420 | "description": "Arguments to the application" 421 | }, 422 | "channel": { 423 | "required": true, 424 | "type": "Channel" 425 | } 426 | } 427 | } 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /sample-api/mailboxes.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2013, Digium, Inc.", 3 | "_author": "Jonathan Rose ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/mailboxes.{format}", 8 | "apis": [ 9 | { 10 | "path": "/mailboxes", 11 | "description": "Mailboxes", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "summary": "List all mailboxes.", 16 | "nickname": "list", 17 | "responseClass": "List[Mailbox]" 18 | } 19 | ] 20 | }, 21 | { 22 | "path": "/mailboxes/{mailboxName}", 23 | "description": "Mailbox state", 24 | "operations": [ 25 | { 26 | "httpMethod": "GET", 27 | "summary": "Retrieve the current state of a mailbox.", 28 | "nickname": "get", 29 | "responseClass": "Mailbox", 30 | "parameters": [ 31 | { 32 | "name": "mailboxName", 33 | "description": "Name of the mailbox", 34 | "paramType": "path", 35 | "required": true, 36 | "allowMultiple": false, 37 | "dataType": "string" 38 | } 39 | ], 40 | "errorResponses": [ 41 | { 42 | "code": 404, 43 | "reason": "Mailbox not found" 44 | } 45 | ] 46 | }, 47 | { 48 | "httpMethod": "PUT", 49 | "summary": "Change the state of a mailbox. (Note - implicitly creates the mailbox).", 50 | "nickname": "update", 51 | "responseClass": "void", 52 | "parameters": [ 53 | { 54 | "name": "mailboxName", 55 | "description": "Name of the mailbox", 56 | "paramType": "path", 57 | "required": true, 58 | "allowMultiple": false, 59 | "dataType": "string" 60 | }, 61 | { 62 | "name": "oldMessages", 63 | "description": "Count of old messages in the mailbox", 64 | "paramType": "query", 65 | "required": true, 66 | "allowMultiple": false, 67 | "dataType": "int" 68 | }, 69 | { 70 | "name": "newMessages", 71 | "description": "Count of new messages in the mailbox", 72 | "paramType": "query", 73 | "required": true, 74 | "allowMultiple": false, 75 | "dataType": "int" 76 | } 77 | ], 78 | "errorResponses": [ 79 | { 80 | "code": 404, 81 | "reason": "Mailbox not found" 82 | } 83 | ] 84 | }, 85 | { 86 | "httpMethod": "DELETE", 87 | "summary": "Destroy a mailbox.", 88 | "nickname": "delete", 89 | "responseClass": "void", 90 | "parameters": [ 91 | { 92 | "name": "mailboxName", 93 | "description": "Name of the mailbox", 94 | "paramType": "path", 95 | "required": true, 96 | "allowMultiple": false, 97 | "dataType": "string" 98 | } 99 | ], 100 | "errorResponses": [ 101 | { 102 | "code": 404, 103 | "reason": "Mailbox not found" 104 | } 105 | ] 106 | } 107 | ] 108 | } 109 | ], 110 | "models": { 111 | "Mailbox": { 112 | "id": "Mailbox", 113 | "description": "Represents the state of a mailbox.", 114 | "properties": { 115 | "name": { 116 | "type": "string", 117 | "description": "Name of the mailbox.", 118 | "required": true 119 | }, 120 | "old_messages": { 121 | "type": "int", 122 | "description": "Count of old messages in the mailbox.", 123 | "required": true 124 | }, 125 | "new_messages": { 126 | "type": "int", 127 | "description": "Count of new messages in the mailbox.", 128 | "required": true 129 | } 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /sample-api/playbacks.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/playbacks.{format}", 8 | "apis": [ 9 | { 10 | "path": "/playbacks/{playbackId}", 11 | "description": "Control object for a playback operation.", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "summary": "Get a playback's details.", 16 | "nickname": "get", 17 | "responseClass": "Playback", 18 | "parameters": [ 19 | { 20 | "name": "playbackId", 21 | "description": "Playback's id", 22 | "paramType": "path", 23 | "required": true, 24 | "allowMultiple": false, 25 | "dataType": "string" 26 | } 27 | ], 28 | "errorResponses": [ 29 | { 30 | "code": 404, 31 | "reason": "The playback cannot be found" 32 | } 33 | ] 34 | }, 35 | { 36 | "httpMethod": "DELETE", 37 | "summary": "Stop a playback.", 38 | "nickname": "stop", 39 | "responseClass": "void", 40 | "parameters": [ 41 | { 42 | "name": "playbackId", 43 | "description": "Playback's id", 44 | "paramType": "path", 45 | "required": true, 46 | "allowMultiple": false, 47 | "dataType": "string" 48 | } 49 | ], 50 | "errorResponses": [ 51 | { 52 | "code": 404, 53 | "reason": "The playback cannot be found" 54 | } 55 | ] 56 | } 57 | ] 58 | }, 59 | { 60 | "path": "/playbacks/{playbackId}/control", 61 | "description": "Control object for a playback operation.", 62 | "operations": [ 63 | { 64 | "httpMethod": "POST", 65 | "summary": "Control a playback.", 66 | "nickname": "control", 67 | "responseClass": "void", 68 | "parameters": [ 69 | { 70 | "name": "playbackId", 71 | "description": "Playback's id", 72 | "paramType": "path", 73 | "required": true, 74 | "allowMultiple": false, 75 | "dataType": "string" 76 | }, 77 | { 78 | "name": "operation", 79 | "description": "Operation to perform on the playback.", 80 | "paramType": "query", 81 | "required": true, 82 | "allowMultiple": false, 83 | "dataType": "string", 84 | "allowableValues": { 85 | "valueType": "LIST", 86 | "values": [ 87 | "restart", 88 | "pause", 89 | "unpause", 90 | "reverse", 91 | "forward" 92 | ] 93 | } 94 | } 95 | ], 96 | "errorResponses": [ 97 | { 98 | "code": 400, 99 | "reason": "The provided operation parameter was invalid" 100 | }, 101 | { 102 | "code": 404, 103 | "reason": "The playback cannot be found" 104 | }, 105 | { 106 | "code": 409, 107 | "reason": "The operation cannot be performed in the playback's current state" 108 | } 109 | ] 110 | } 111 | ] 112 | } 113 | ], 114 | "models": { 115 | "Playback": { 116 | "id": "Playback", 117 | "description": "Object representing the playback of media to a channel", 118 | "properties": { 119 | "id": { 120 | "type": "string", 121 | "description": "ID for this playback operation", 122 | "required": true 123 | }, 124 | "media_uri": { 125 | "type": "string", 126 | "description": "URI for the media to play back.", 127 | "required": true 128 | }, 129 | "target_uri": { 130 | "type": "string", 131 | "description": "URI for the channel or bridge to play the media on", 132 | "required": true 133 | }, 134 | "language": { 135 | "type": "string", 136 | "description": "For media types that support multiple languages, the language requested for playback." 137 | }, 138 | "state": { 139 | "type": "string", 140 | "description": "Current state of the playback operation.", 141 | "required": true, 142 | "allowableValues": { 143 | "valueType": "LIST", 144 | "values": [ 145 | "queued", 146 | "playing", 147 | "complete" 148 | ] 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /sample-api/recordings.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/recordings.{format}", 8 | "apis": [ 9 | { 10 | "path": "/recordings/stored", 11 | "description": "Recordings", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "summary": "List recordings that are complete.", 16 | "nickname": "listStored", 17 | "responseClass": "List[StoredRecording]" 18 | } 19 | ] 20 | }, 21 | { 22 | "path": "/recordings/stored/{recordingName}", 23 | "description": "Individual recording", 24 | "operations": [ 25 | { 26 | "httpMethod": "GET", 27 | "summary": "Get a stored recording's details.", 28 | "nickname": "getStored", 29 | "responseClass": "StoredRecording", 30 | "parameters": [ 31 | { 32 | "name": "recordingName", 33 | "description": "The name of the recording", 34 | "paramType": "path", 35 | "required": true, 36 | "allowMultiple": false, 37 | "dataType": "string" 38 | } 39 | ], 40 | "errorResponses": [ 41 | { 42 | "code": 404, 43 | "reason": "Recording not found" 44 | } 45 | ] 46 | }, 47 | { 48 | "httpMethod": "DELETE", 49 | "summary": "Delete a stored recording.", 50 | "nickname": "deleteStored", 51 | "responseClass": "void", 52 | "parameters": [ 53 | { 54 | "name": "recordingName", 55 | "description": "The name of the recording", 56 | "paramType": "path", 57 | "required": true, 58 | "allowMultiple": false, 59 | "dataType": "string" 60 | } 61 | ], 62 | "errorResponses": [ 63 | { 64 | "code": 404, 65 | "reason": "Recording not found" 66 | } 67 | ] 68 | } 69 | ] 70 | }, 71 | { 72 | "path": "/recordings/live/{recordingName}", 73 | "description": "A recording that is in progress", 74 | "operations": [ 75 | { 76 | "httpMethod": "GET", 77 | "summary": "List live recordings.", 78 | "nickname": "getLive", 79 | "responseClass": "LiveRecording", 80 | "parameters": [ 81 | { 82 | "name": "recordingName", 83 | "description": "The name of the recording", 84 | "paramType": "path", 85 | "required": true, 86 | "allowMultiple": false, 87 | "dataType": "string" 88 | } 89 | ], 90 | "errorResponses": [ 91 | { 92 | "code": 404, 93 | "reason": "Recording not found" 94 | } 95 | ] 96 | }, 97 | { 98 | "httpMethod": "DELETE", 99 | "summary": "Stop a live recording and discard it.", 100 | "nickname": "cancel", 101 | "responseClass": "void", 102 | "parameters": [ 103 | { 104 | "name": "recordingName", 105 | "description": "The name of the recording", 106 | "paramType": "path", 107 | "required": true, 108 | "allowMultiple": false, 109 | "dataType": "string" 110 | } 111 | ], 112 | "errorResponses": [ 113 | { 114 | "code": 404, 115 | "reason": "Recording not found" 116 | } 117 | ] 118 | } 119 | ] 120 | }, 121 | { 122 | "path": "/recordings/live/{recordingName}/stop", 123 | "operations": [ 124 | { 125 | "httpMethod": "POST", 126 | "summary": "Stop a live recording and store it.", 127 | "nickname": "stop", 128 | "responseClass": "void", 129 | "parameters": [ 130 | { 131 | "name": "recordingName", 132 | "description": "The name of the recording", 133 | "paramType": "path", 134 | "required": true, 135 | "allowMultiple": false, 136 | "dataType": "string" 137 | } 138 | ], 139 | "errorResponses": [ 140 | { 141 | "code": 404, 142 | "reason": "Recording not found" 143 | } 144 | ] 145 | } 146 | ] 147 | }, 148 | { 149 | "path": "/recordings/live/{recordingName}/pause", 150 | "operations": [ 151 | { 152 | "httpMethod": "POST", 153 | "summary": "Pause a live recording.", 154 | "notes": "Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused. Paused time is not included in the accounting for maxDurationSeconds.", 155 | "nickname": "pause", 156 | "responseClass": "void", 157 | "parameters": [ 158 | { 159 | "name": "recordingName", 160 | "description": "The name of the recording", 161 | "paramType": "path", 162 | "required": true, 163 | "allowMultiple": false, 164 | "dataType": "string" 165 | } 166 | ], 167 | "errorResponses": [ 168 | { 169 | "code": 404, 170 | "reason": "Recording not found" 171 | }, 172 | { 173 | "code": 409, 174 | "reason": "Recording not in session" 175 | } 176 | ] 177 | }, 178 | { 179 | "httpMethod": "DELETE", 180 | "summary": "Unpause a live recording.", 181 | "nickname": "unpause", 182 | "responseClass": "void", 183 | "parameters": [ 184 | { 185 | "name": "recordingName", 186 | "description": "The name of the recording", 187 | "paramType": "path", 188 | "required": true, 189 | "allowMultiple": false, 190 | "dataType": "string" 191 | } 192 | ], 193 | "errorResponses": [ 194 | { 195 | "code": 404, 196 | "reason": "Recording not found" 197 | }, 198 | { 199 | "code": 409, 200 | "reason": "Recording not in session" 201 | } 202 | ] 203 | } 204 | ] 205 | }, 206 | { 207 | "path": "/recordings/live/{recordingName}/mute", 208 | "operations": [ 209 | { 210 | "httpMethod": "POST", 211 | "summary": "Mute a live recording.", 212 | "notes": "Muting a recording suspends silence detection, which will be restarted when the recording is unmuted.", 213 | "nickname": "mute", 214 | "responseClass": "void", 215 | "parameters": [ 216 | { 217 | "name": "recordingName", 218 | "description": "The name of the recording", 219 | "paramType": "path", 220 | "required": true, 221 | "allowMultiple": false, 222 | "dataType": "string" 223 | } 224 | ], 225 | "errorResponses": [ 226 | { 227 | "code": 404, 228 | "reason": "Recording not found" 229 | }, 230 | { 231 | "code": 409, 232 | "reason": "Recording not in session" 233 | } 234 | ] 235 | }, 236 | { 237 | "httpMethod": "DELETE", 238 | "summary": "Unmute a live recording.", 239 | "nickname": "unmute", 240 | "responseClass": "void", 241 | "parameters": [ 242 | { 243 | "name": "recordingName", 244 | "description": "The name of the recording", 245 | "paramType": "path", 246 | "required": true, 247 | "allowMultiple": false, 248 | "dataType": "string" 249 | } 250 | ], 251 | "errorResponses": [ 252 | { 253 | "code": 404, 254 | "reason": "Recording not found" 255 | }, 256 | { 257 | "code": 409, 258 | "reason": "Recording not in session" 259 | } 260 | ] 261 | } 262 | ] 263 | } 264 | ], 265 | "models": { 266 | "StoredRecording": { 267 | "id": "StoredRecording", 268 | "description": "A past recording that may be played back.", 269 | "properties": { 270 | "name": { 271 | "required": true, 272 | "type": "string" 273 | }, 274 | "format": { 275 | "required": true, 276 | "type": "string" 277 | } 278 | } 279 | }, 280 | "LiveRecording": { 281 | "id": "LiveRecording", 282 | "description": "A recording that is in progress", 283 | "properties": { 284 | "name": { 285 | "required": true, 286 | "type": "string", 287 | "description": "Base name for the recording" 288 | }, 289 | "format": { 290 | "required": true, 291 | "type": "string", 292 | "description": "Recording format (wav, gsm, etc.)" 293 | }, 294 | "state": { 295 | "required": false, 296 | "type": "string", 297 | "allowableValues": { 298 | "valueType": "LIST", 299 | "values": [ 300 | "queued", 301 | "recording", 302 | "paused", 303 | "done", 304 | "failed", 305 | "canceled" 306 | ] 307 | } 308 | }, 309 | "state": { 310 | "required": true, 311 | "type": "string" 312 | }, 313 | "format": { 314 | "required": true, 315 | "type": "string" 316 | } 317 | } 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /sample-api/resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "apis": [ 8 | { 9 | "path": "/api-docs/asterisk.{format}", 10 | "description": "Asterisk resources" 11 | }, 12 | { 13 | "path": "/api-docs/endpoints.{format}", 14 | "description": "Endpoint resources" 15 | }, 16 | { 17 | "path": "/api-docs/channels.{format}", 18 | "description": "Channel resources" 19 | }, 20 | { 21 | "path": "/api-docs/bridges.{format}", 22 | "description": "Bridge resources" 23 | }, 24 | { 25 | "path": "/api-docs/recordings.{format}", 26 | "description": "Recording resources" 27 | }, 28 | { 29 | "path": "/api-docs/sounds.{format}", 30 | "description": "Sound resources" 31 | }, 32 | { 33 | "path": "/api-docs/mailboxes.{format}", 34 | "description": "Mailbox (MWI) resources" 35 | }, 36 | { 37 | "path": "/api-docs/playbacks.{format}", 38 | "description": "Playback control resources" 39 | }, 40 | { 41 | "path": "/api-docs/deviceStates.{format}", 42 | "description": "Device state resources" 43 | }, 44 | { 45 | "path": "/api-docs/events.{format}", 46 | "description": "WebSocket resource" 47 | }, 48 | { 49 | "path": "/api-docs/applications.{format}", 50 | "description": "Stasis application resources" 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /sample-api/sounds.json: -------------------------------------------------------------------------------- 1 | { 2 | "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.", 3 | "_author": "David M. Lee, II ", 4 | "apiVersion": "0.0.0-test", 5 | "swaggerVersion": "1.1", 6 | "basePath": "http://ari.py/ari", 7 | "resourcePath": "/api-docs/sounds.{format}", 8 | "apis": [ 9 | { 10 | "path": "/sounds", 11 | "description": "Sounds", 12 | "operations": [ 13 | { 14 | "httpMethod": "GET", 15 | "summary": "List all sounds.", 16 | "nickname": "list", 17 | "responseClass": "List[Sound]", 18 | "parameters": [ 19 | { 20 | "name": "lang", 21 | "description": "Lookup sound for a specific language.", 22 | "paramType": "query", 23 | "dataType": "string", 24 | "required": false 25 | }, 26 | { 27 | "name": "format", 28 | "description": "Lookup sound in a specific format.", 29 | "paramType": "query", 30 | "dataType": "string", 31 | "required": false, 32 | "__note": "core show translation can show translation paths between formats, along with relative costs. so this could be just installed format, or we could follow that for transcoded formats." 33 | } 34 | ] 35 | } 36 | ] 37 | }, 38 | { 39 | "path": "/sounds/{soundId}", 40 | "description": "Individual sound", 41 | "operations": [ 42 | { 43 | "httpMethod": "GET", 44 | "summary": "Get a sound's details.", 45 | "nickname": "get", 46 | "responseClass": "Sound", 47 | "parameters": [ 48 | { 49 | "name": "soundId", 50 | "description": "Sound's id", 51 | "paramType": "path", 52 | "required": true, 53 | "allowMultiple": false, 54 | "dataType": "string" 55 | } 56 | ] 57 | } 58 | ] 59 | } 60 | ], 61 | "models": { 62 | "FormatLangPair": { 63 | "id": "FormatLangPair", 64 | "description": "Identifies the format and language of a sound file", 65 | "properties": { 66 | "language": { 67 | "required": true, 68 | "type": "string" 69 | }, 70 | "format": { 71 | "required": true, 72 | "type": "string" 73 | } 74 | } 75 | }, 76 | "Sound": { 77 | "id": "Sound", 78 | "description": "A media file that may be played back.", 79 | "properties": { 80 | "id": { 81 | "required": true, 82 | "description": "Sound's identifier.", 83 | "type": "string" 84 | }, 85 | "text": { 86 | "required": false, 87 | "description": "Text description of the sound, usually the words spoken.", 88 | "type": "string" 89 | }, 90 | "formats": { 91 | "required": true, 92 | "description": "The formats and languages in which this sound is available.", 93 | "type": "List[FormatLangPair]" 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [nosetests] 5 | config = nose.cfg 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Copyright (c) 2013, Digium, Inc. 5 | # 6 | 7 | import os 8 | 9 | from setuptools import setup 10 | 11 | setup( 12 | name="ari", 13 | version="0.1.3", 14 | license="BSD 3-Clause License", 15 | description="Library for accessing the Asterisk REST Interface", 16 | long_description=open(os.path.join(os.path.dirname(__file__), 17 | "README.rst")).read(), 18 | author="Digium, Inc.", 19 | author_email="dlee@digium.com", 20 | url="https://github.com/asterisk/asterisk_rest_libraries", 21 | packages=["ari"], 22 | classifiers=[ 23 | "Development Status :: 1 - Planning", 24 | "Intended Audience :: Developers", 25 | "Topic :: Software Development :: Libraries :: Python Modules", 26 | "License :: OSI Approved :: BSD License", 27 | "Operating System :: OS Independent", 28 | "Programming Language :: Python", 29 | ], 30 | tests_require=["coverage", "httpretty", "nose", "tissue"], 31 | install_requires=["swaggerpy"], 32 | ) 33 | --------------------------------------------------------------------------------