Skip to content

Commit 85231f4

Browse files
normanarguetaManik Sachdeva
authored andcommitted
Adding Signaling functionality (#128)
* Add signaling API
1 parent f738b51 commit 85231f4

File tree

4 files changed

+156
-7
lines changed

4 files changed

+156
-7
lines changed

README.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,27 @@ Note that you can also create an automatically archived session, by passing in
210210
For more information on archiving, see the
211211
`OpenTok archiving <https://tokbox.com/opentok/tutorials/archiving/>`_ programming guide.
212212

213+
Sending Signals
214+
~~~~~~~~~~~~~~~~~~~~~
215+
216+
Once a Session is created, you can send signals to everyone in the session or to a specific connection. You can send a signal by calling the ``signal(session_id, payload)`` method of the ``OpenTok`` class. The ``payload`` parameter is a dictionary used to set the ``type``, ``data`` fields. Ỳou can also call the method with the parameter ``connection_id`` to send a signal to a specific connection ``signal(session_id, data, connection_id)``.
217+
218+
.. code:: python
219+
220+
# payload structure
221+
payload = {
222+
'type': 'type', #optional
223+
'data': 'signal data' #required
224+
}
225+
226+
connection_id = '2a84cd30-3a33-917f-9150-49e454e01572'
227+
228+
# To send a signal to everyone in the session:
229+
opentok.signal(session_id, payload)
230+
231+
# To send a signal to a specific connection in the session:
232+
opentok.signal(session_id, payload, connection_id)
233+
213234
214235
Samples
215236
-------

opentok/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@ class ArchiveError(OpenTokException):
3030
of the requested archive is invalid.
3131
"""
3232
pass
33+
34+
class SignalingError(OpenTokException):
35+
"""Indicates that there was a signaling specific problem, one of the parameter
36+
is invalid or the type|data string doesn't have a correct size"""
37+
pass

opentok/opentok.py

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .version import __version__
2222
from .session import Session
2323
from .archives import Archive, ArchiveList, OutputModes
24-
from .exceptions import OpenTokException, RequestError, AuthError, NotFoundError, ArchiveError
24+
from .exceptions import OpenTokException, RequestError, AuthError, NotFoundError, ArchiveError, SignalingError
2525

2626
class Roles(Enum):
2727
"""List of valid roles for a token."""
@@ -293,7 +293,7 @@ def headers(self):
293293
'X-OPENTOK-AUTH': self._create_jwt_auth_header()
294294
}
295295

296-
def archive_headers(self):
296+
def json_headers(self):
297297
"""For internal use."""
298298
result = self.headers()
299299
result['Content-Type'] = 'application/json'
@@ -311,6 +311,17 @@ def archive_url(self, archive_id=None):
311311
url = url + '/' + archive_id
312312
return url
313313

314+
def signaling_url(self, session_id, connection_id=None):
315+
"""For internal use."""
316+
url = self.api_url + '/v2/project/' + self.api_key + '/session/' + session_id
317+
318+
if connection_id:
319+
url += '/connection/' + connection_id
320+
321+
url += '/signal'
322+
323+
return url
324+
314325
def start_archive(self, session_id, has_audio=True, has_video=True, name=None, output_mode=OutputModes.composed, resolution=None):
315326
"""
316327
Starts archiving an OpenTok session.
@@ -361,7 +372,7 @@ def start_archive(self, session_id, has_audio=True, has_video=True, name=None, o
361372
'resolution': resolution,
362373
}
363374

364-
response = requests.post(self.archive_url(), data=json.dumps(payload), headers=self.archive_headers(), proxies=self.proxies, timeout=self.timeout)
375+
response = requests.post(self.archive_url(), data=json.dumps(payload), headers=self.json_headers(), proxies=self.proxies, timeout=self.timeout)
365376

366377
if response.status_code < 300:
367378
return Archive(self, response.json())
@@ -394,7 +405,7 @@ def stop_archive(self, archive_id):
394405
395406
:rtype: The Archive object corresponding to the archive being stopped.
396407
"""
397-
response = requests.post(self.archive_url(archive_id) + '/stop', headers=self.archive_headers(), proxies=self.proxies, timeout=self.timeout)
408+
response = requests.post(self.archive_url(archive_id) + '/stop', headers=self.json_headers(), proxies=self.proxies, timeout=self.timeout)
398409

399410
if response.status_code < 300:
400411
return Archive(self, response.json())
@@ -417,7 +428,7 @@ def delete_archive(self, archive_id):
417428
418429
:param String archive_id: The archive ID of the archive to be deleted.
419430
"""
420-
response = requests.delete(self.archive_url(archive_id), headers=self.archive_headers(), proxies=self.proxies, timeout=self.timeout)
431+
response = requests.delete(self.archive_url(archive_id), headers=self.json_headers(), proxies=self.proxies, timeout=self.timeout)
421432

422433
if response.status_code < 300:
423434
pass
@@ -435,7 +446,7 @@ def get_archive(self, archive_id):
435446
436447
:rtype: The Archive object.
437448
"""
438-
response = requests.get(self.archive_url(archive_id), headers=self.archive_headers(), proxies=self.proxies, timeout=self.timeout)
449+
response = requests.get(self.archive_url(archive_id), headers=self.json_headers(), proxies=self.proxies, timeout=self.timeout)
439450

440451
if response.status_code < 300:
441452
return Archive(self, response.json())
@@ -464,7 +475,7 @@ def get_archives(self, offset=None, count=None):
464475
if count is not None:
465476
params['count'] = count
466477

467-
response = requests.get(self.archive_url() + "?" + urlencode(params), headers=self.archive_headers(), proxies=self.proxies, timeout=self.timeout)
478+
response = requests.get(self.archive_url() + "?" + urlencode(params), headers=self.json_headers(), proxies=self.proxies, timeout=self.timeout)
468479

469480
if response.status_code < 300:
470481
return ArchiveList(self, response.json())
@@ -475,6 +486,42 @@ def get_archives(self, offset=None, count=None):
475486
else:
476487
raise RequestError("An unexpected error occurred", response.status_code)
477488

489+
def signal(self, session_id, payload, connection_id=None):
490+
"""
491+
Send signals to all participants in an active OpenTok session or to a specific client
492+
connected to that session.
493+
494+
:param String session_id: The session ID of the OpenTok session that receives the signal
495+
496+
:param Dictionary payload: Structure that contains both the type and data fields. These
497+
correspond to the type and data parameters passed in the client signal received handlers
498+
499+
:param String connection_id: The connection_id parameter is an optional string used to
500+
specify the connection ID of a client connected to the session. If you specify this value,
501+
the signal is sent to the specified client. Otherwise, the signal is sent to all clients
502+
connected to the session
503+
"""
504+
response = requests.post(
505+
self.signaling_url(session_id, connection_id),
506+
data=json.dumps(payload),
507+
headers=self.json_headers(),
508+
proxies=self.proxies,
509+
timeout=self.timeout
510+
)
511+
512+
if response.status_code == 204:
513+
pass
514+
elif response.status_code == 400:
515+
raise SignalingError('One of the signal properties - data, type, sessionId or connectionId - is invalid.')
516+
elif response.status_code == 403:
517+
raise AuthError('You are not authorized to send the signal. Check your authentication credentials.')
518+
elif response.status_code == 404:
519+
raise SignalingError('The client specified by the connectionId property is not connected to the session.')
520+
elif response.status_code == 413:
521+
raise SignalingError('The type string exceeds the maximum length (128 bytes), or the data string exceeds the maximum size (8 kB).')
522+
else:
523+
raise RequestError("An unexpected error occurred", response.status_code)
524+
478525
def _sign_string(self, string, secret):
479526
return hmac.new(secret.encode('utf-8'), string.encode('utf-8'), hashlib.sha1).hexdigest()
480527

tests/test_signal.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import unittest
2+
from six import text_type, u, b, PY2, PY3
3+
from opentok import OpenTok, Session, __version__
4+
import httpretty
5+
import json
6+
import textwrap
7+
from expects import *
8+
9+
from .validate_jwt import validate_jwt_header
10+
11+
class OpenTokSignalTest(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+
self.session_id = u('SESSIONID')
17+
18+
@httpretty.activate
19+
def test_signal(self):
20+
data = {
21+
u('type'): u('type test'),
22+
u('data'): u('test data')
23+
}
24+
25+
httpretty.register_uri(httpretty.POST, u('https://api.opentok.com/v2/project/{0}/session/{1}/signal').format(self.api_key, self.session_id),
26+
body=textwrap.dedent(u("""\
27+
{
28+
"type": "type test",
29+
"data": "test data"
30+
}""")),
31+
status=204,
32+
content_type=u('application/json'))
33+
34+
self.opentok.signal(self.session_id, data)
35+
36+
validate_jwt_header(self, httpretty.last_request().headers[u('x-opentok-auth')])
37+
expect(httpretty.last_request().headers[u('user-agent')]).to(contain(u('OpenTok-Python-SDK/')+__version__))
38+
expect(httpretty.last_request().headers[u('content-type')]).to(equal(u('application/json')))
39+
# non-deterministic json encoding. have to decode to test it properly
40+
if PY2:
41+
body = json.loads(httpretty.last_request().body)
42+
if PY3:
43+
body = json.loads(httpretty.last_request().body.decode('utf-8'))
44+
expect(body).to(have_key(u('type'), u('type test')))
45+
expect(body).to(have_key(u('data'), u('test data')))
46+
47+
@httpretty.activate
48+
def test_signal_with_connection_id(self):
49+
data = {
50+
u('type'): u('type test'),
51+
u('data'): u('test data')
52+
}
53+
54+
connection_id = u('da9cb410-e29b-4c2d-ab9e-fe65bf83fcaf')
55+
56+
httpretty.register_uri(httpretty.POST, u('https://api.opentok.com/v2/project/{0}/session/{1}/connection/{2}/signal').format(self.api_key, self.session_id, connection_id),
57+
body=textwrap.dedent(u("""\
58+
{
59+
"type": "type test",
60+
"data": "test data"
61+
}""")),
62+
status=204,
63+
content_type=u('application/json'))
64+
65+
self.opentok.signal(self.session_id, data, connection_id)
66+
67+
validate_jwt_header(self, httpretty.last_request().headers[u('x-opentok-auth')])
68+
expect(httpretty.last_request().headers[u('user-agent')]).to(contain(u('OpenTok-Python-SDK/')+__version__))
69+
expect(httpretty.last_request().headers[u('content-type')]).to(equal(u('application/json')))
70+
# non-deterministic json encoding. have to decode to test it properly
71+
if PY2:
72+
body = json.loads(httpretty.last_request().body)
73+
if PY3:
74+
body = json.loads(httpretty.last_request().body.decode('utf-8'))
75+
expect(body).to(have_key(u('type'), u('type test')))
76+
expect(body).to(have_key(u('data'), u('test data')))

0 commit comments

Comments
 (0)