Skip to content

Commit 2d59858

Browse files
normanarguetaManik Sachdeva
authored andcommitted
Adding dial() method (#143)
* Adding dial() method
1 parent 1cd1ac3 commit 2d59858

File tree

7 files changed

+250
-1
lines changed

7 files changed

+250
-1
lines changed

README.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,40 @@ Your application server can disconnect a client from an OpenTok session by calli
307307
# To send a request to disconnect a client:
308308
opentok.force_disconnect(session_id, connection_id)
309309
310+
Working with SIP Interconnect
311+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
312+
313+
You can connect your SIP platform to an OpenTok session, the audio from your end of the SIP call is added to the OpenTok session as an audio-only stream. The OpenTok Media Router mixes audio from other streams in the session and sends the mixed audio to your SIP endpoint.
314+
315+
.. code:: python
316+
317+
session_id = u('SESSIONID')
318+
token = u('TOKEN')
319+
sip_uri = u('sip:[email protected];transport=tls')
320+
321+
# call the method with the required parameters
322+
sip_call = opentok.dial(session_id, token, sip_uri)
323+
324+
# the method also support aditional options to establish the sip call
325+
326+
options = {
327+
'from': '[email protected]',
328+
'headers': {
329+
'headerKey': 'headerValue'
330+
},
331+
'auth': {
332+
'username': 'username',
333+
'password': 'password'
334+
},
335+
'secure': True
336+
}
337+
338+
# call the method with aditional options
339+
sip_call = opentok.dial(session_id, token, sip_uri, options)
340+
341+
For more information, including technical details and security considerations, see the
342+
`OpenTok SIP interconnect <https://tokbox.com/developer/guides/sip/>`_ developer guide.
343+
310344
Samples
311345
-------
312346

opentok/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
from .version import __version__
66
from .stream import Stream
77
from .streamlist import StreamList
8+
from .sip_call import SipCall

opentok/endpoints.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,8 @@ def set_archive_layout_url(self, archive_id):
4545
""" this method returns the url to set the archive layout """
4646
url = self.api_url + '/v2/project/' + self.api_key + '/archive/' + archive_id + '/layout'
4747
return url
48+
49+
def dial_url(self):
50+
""" this method returns the url to initialize a SIP call """
51+
url = self.api_url + '/v2/project/' + self.api_key + '/dial'
52+
return url

opentok/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,11 @@ class ForceDisconnectError(OpenTokException):
4848
is not connected to the session
4949
"""
5050
pass
51+
52+
class SipDialError(OpenTokException):
53+
"""
54+
Indicates that there was a SIP dial specific problem:
55+
The Session ID passed in is invalid or you attempt to start a SIP call for a session
56+
that does not use the OpenTok Media Router.
57+
"""
58+
pass

opentok/opentok.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .archives import Archive, ArchiveList, OutputModes
2525
from .stream import Stream
2626
from .streamlist import StreamList
27+
from .sip_call import SipCall
2728
from .exceptions import (
2829
OpenTokException,
2930
RequestError,
@@ -32,7 +33,8 @@
3233
ArchiveError,
3334
SignalingError,
3435
GetStreamError,
35-
ForceDisconnectError
36+
ForceDisconnectError,
37+
SipDialError
3638
)
3739

3840
class Roles(Enum):
@@ -636,6 +638,88 @@ def set_archive_layout(self, archive_id, layout_type, stylesheet=None):
636638
else:
637639
raise RequestError('OpenTok server error.', response.status_code)
638640

641+
642+
def dial(self, session_id, token, sip_uri, options=[]):
643+
"""
644+
Use this method to connect a SIP platform to an OpenTok session. The audio from the end
645+
of the SIP call is added to the OpenTok session as an audio-only stream. The OpenTok Media
646+
Router mixes audio from other streams in the session and sends the mixed audio to the SIP
647+
endpoint
648+
649+
:param String session_id: The OpenTok session ID for the SIP call to join
650+
651+
:param String token: The OpenTok token to be used for the participant being called
652+
653+
:param String sip_uri: The SIP URI to be used as destination of the SIP call initiated from
654+
OpenTok to the SIP platform
655+
656+
:param Dictionary options optional: Aditional options with the following properties:
657+
658+
String 'from': The number or string that will be sent to the final SIP number
659+
as the caller
660+
661+
Dictionary 'headers': Defines custom headers to be added to the SIP INVITE request
662+
initiated from OpenTok to the SIP platform. Each of the custom headers must
663+
start with the "X-" prefix, or the call will result in a Bad Request (400) response
664+
665+
Dictionary 'auth': Contains the username and password to be used in the the SIP
666+
INVITE request for HTTP digest authentication, if it is required by the SIP platform
667+
For example:
668+
669+
'auth': {
670+
'username': 'username',
671+
'password': 'password'
672+
}
673+
674+
Boolean 'secure': A Boolean flag that indicates whether the media must be transmitted
675+
encrypted (true) or not (false, the default)
676+
677+
:rtype: A SipCall object, which contains data of the SIP call: id, connectionId and streamId
678+
"""
679+
payload = {
680+
'sessionId': session_id,
681+
'token': token,
682+
'sip': {
683+
'uri': sip_uri
684+
}
685+
}
686+
687+
if 'from' in options:
688+
payload['sip']['from'] = options['from']
689+
690+
if 'headers' in options:
691+
payload['sip']['headers'] = options['headers']
692+
693+
if 'auth' in options:
694+
payload['sip']['auth'] = options['auth']
695+
696+
if 'secure' in options:
697+
payload['sip']['secure'] = options['secure']
698+
699+
endpoint = self.endpoints.dial_url()
700+
response = requests.post(
701+
endpoint,
702+
data=json.dumps(payload),
703+
headers=self.json_headers(),
704+
proxies=self.proxies,
705+
timeout=self.timeout
706+
)
707+
708+
if response.status_code == 200:
709+
return SipCall(response.json())
710+
elif response.status_code == 400:
711+
raise SipDialError('Invalid request. Invalid session ID.')
712+
elif response.status_code == 403:
713+
raise AuthError('Authentication error.')
714+
elif response.status_code == 404:
715+
raise SipDialError('The session does not exist.')
716+
elif response.status_code == 409:
717+
raise SipDialError(
718+
'You attempted to start a SIP call for a session that '
719+
'does not use the OpenTok Media Router.')
720+
else:
721+
raise RequestError('OpenTok server error.', response.status_code)
722+
639723
def _sign_string(self, string, secret):
640724
return hmac.new(secret.encode('utf-8'), string.encode('utf-8'), hashlib.sha1).hexdigest()
641725

opentok/sip_call.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import json
2+
3+
class SipCall(object):
4+
"""
5+
Represents data from a SIP call
6+
"""
7+
8+
def __init__(self, kwargs):
9+
self.id = kwargs.get('id')
10+
self.connectionId = kwargs.get('connectionId')
11+
self.streamId = kwargs.get('streamId')
12+
13+
def json(self):
14+
"""
15+
Returns a JSON representation of the SIP call
16+
"""
17+
return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4)

tests/test_sip_call.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import unittest
2+
import textwrap
3+
import httpretty
4+
5+
from six import u
6+
from expects import *
7+
from opentok import OpenTok, SipCall, __version__
8+
9+
from .validate_jwt import validate_jwt_header
10+
11+
class OpenTokSipCallTest(unittest.TestCase):
12+
def setUp(self):
13+
self.api_key = u('123456')
14+
self.api_secret = u('1234567890abcdef1234567890abcdef1234567890')
15+
self.opentok = OpenTok(self.api_key, self.api_secret)
16+
17+
self.session_id = u('SESSIONID')
18+
self.token = u('TOKEN')
19+
self.sip_uri = u('sip:[email protected];transport=tls')
20+
21+
@httpretty.activate
22+
def test_sip_call_with_required_parameters(self):
23+
"""
24+
Test dial() method using just the required parameters: session_id, token, sip_uri
25+
"""
26+
sip_call = SipCall({
27+
u('id'): u('b0a5a8c7-dc38-459f-a48d-a7f2008da853'),
28+
u('connectionId'): u('e9f8c166-6c67-440d-994a-04fb6dfed007'),
29+
u('streamId'): u('482bce73-f882-40fd-8ca5-cb74ff416036')
30+
})
31+
32+
httpretty.register_uri(
33+
httpretty.POST,
34+
u('https://api.opentok.com/v2/project/{0}/dial').format(self.api_key),
35+
body=textwrap.dedent(u("""\
36+
{
37+
"id": "b0a5a8c7-dc38-459f-a48d-a7f2008da853",
38+
"connectionId": "e9f8c166-6c67-440d-994a-04fb6dfed007",
39+
"streamId": "482bce73-f882-40fd-8ca5-cb74ff416036"
40+
}""")),
41+
status=200,
42+
content_type=u('application/json')
43+
)
44+
45+
sip_call_response = self.opentok.dial(self.session_id, self.token, self.sip_uri)
46+
validate_jwt_header(self, httpretty.last_request().headers[u('x-opentok-auth')])
47+
expect(httpretty.last_request().headers[u('user-agent')]).to(contain(
48+
u('OpenTok-Python-SDK/')+__version__))
49+
expect(httpretty.last_request().headers[u('content-type')]).to(equal(u('application/json')))
50+
expect(sip_call_response).to(be_an(SipCall))
51+
expect(sip_call_response).to(have_property(u('id'), sip_call.id))
52+
expect(sip_call_response).to(have_property(u('connectionId'), sip_call.connectionId))
53+
expect(sip_call_response).to(have_property(u('streamId'), sip_call.streamId))
54+
55+
@httpretty.activate
56+
def test_sip_call_with_aditional_options(self):
57+
"""
58+
Test dial() method with aditional options
59+
"""
60+
sip_call = SipCall({
61+
u('id'): u('b0a5a8c7-dc38-459f-a48d-a7f2008da853'),
62+
u('connectionId'): u('e9f8c166-6c67-440d-994a-04fb6dfed007'),
63+
u('streamId'): u('482bce73-f882-40fd-8ca5-cb74ff416036')
64+
})
65+
66+
httpretty.register_uri(
67+
httpretty.POST,
68+
u('https://api.opentok.com/v2/project/{0}/dial').format(self.api_key),
69+
body=textwrap.dedent(u("""\
70+
{
71+
"id": "b0a5a8c7-dc38-459f-a48d-a7f2008da853",
72+
"connectionId": "e9f8c166-6c67-440d-994a-04fb6dfed007",
73+
"streamId": "482bce73-f882-40fd-8ca5-cb74ff416036"
74+
}""")),
75+
status=200,
76+
content_type=u('application/json')
77+
)
78+
79+
# aditional options to establish the sip call
80+
options = {
81+
'from': '[email protected]',
82+
'headers': {
83+
'headerKey': 'headerValue'
84+
},
85+
'auth': {
86+
'username': 'username',
87+
'password': 'password'
88+
},
89+
'secure': True
90+
}
91+
92+
sip_call_response = self.opentok.dial(self.session_id, self.token, self.sip_uri, options)
93+
validate_jwt_header(self, httpretty.last_request().headers[u('x-opentok-auth')])
94+
expect(httpretty.last_request().headers[u('user-agent')]).to(contain(
95+
u('OpenTok-Python-SDK/')+__version__))
96+
expect(httpretty.last_request().headers[u('content-type')]).to(equal(u('application/json')))
97+
expect(sip_call_response).to(be_an(SipCall))
98+
expect(sip_call_response).to(have_property(u('id'), sip_call.id))
99+
expect(sip_call_response).to(have_property(u('connectionId'), sip_call.connectionId))
100+
expect(sip_call_response).to(have_property(u('streamId'), sip_call.streamId))

0 commit comments

Comments
 (0)