├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Unit_Tests.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Unit_Tests__coverage_.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.idea/scopes/scope_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/testrunner.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 |
16 |
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 |
--------------------------------------------------------------------------------