Skip to content

Commit 10372d0

Browse files
Merge pull request #207 from andersonrocha0/add_user_authentication_and_send_message
Add user authentication and the possibility to trigger/send a message…
2 parents 239d67b + 2b8fcc2 commit 10372d0

12 files changed

+253
-125
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
fail-fast: false
1313
matrix:
14-
python: [2.7, 3.6, 3.7, 3.8, "3.10"]
14+
python: [3.6, 3.7, 3.8, "3.10"]
1515

1616
name: Python ${{ matrix.python }} Test
1717

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ var/
1919

2020
pip-log.txt
2121
pip-delete-this-directory.txt
22+
23+
.idea

README.md

+63-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ In order to use this library, you need to have a free account on <http://pusher.
99

1010
## Supported Platforms
1111

12-
* Python - supports Python versions 2.7, 3.6 and above
12+
* Python - supports Python version 3.6 and above
1313

1414
## Features
1515

@@ -23,11 +23,13 @@ In order to use this library, you need to have a free account on <http://pusher.
2323
- [Getting started](#getting-started)
2424
- [Configuration](#configuration)
2525
- [Triggering Events](#triggering-events)
26+
- [Send a message to a specific user](#send-a-message-to-a-specific-user)
2627
- [Querying Application State](#querying-application-state)
2728
- [Getting Information For All Channels](#getting-information-for-all-channels)
2829
- [Getting Information For A Specific Channel](#getting-information-for-a-specific-channel)
2930
- [Getting User Information For A Presence Channel](#getting-user-information-for-a-presence-channel)
3031
- [Authenticating Channel Subscription](#authenticating-channel-subscription)
32+
- [Authenticating User](#authenticating-user)
3133
- [Terminating User Connections](#terminating-user-connections)
3234
- [End-to-end Encryption](#end-to-end-encryption)
3335
- [Receiving Webhooks](#receiving-webhooks)
@@ -164,6 +166,30 @@ pusher_client.trigger_batch([
164166
])
165167
```
166168

169+
### Send a message to a specific user
170+
171+
#### `Pusher::send_to_user`
172+
173+
|Argument |Description |
174+
|:-:|:-:|
175+
|user_id `String` |**Required** <br> The user id |
176+
|event `String`| **Required** <br> The name of the event you wish to trigger. |
177+
|data `JSONable data` | **Required** <br> The event's payload |
178+
179+
|Return Values |Description |
180+
|:-:|:-:|
181+
|buffered_events `Dict` | A parsed response that includes the event_id for each event published to a channel. See example. |
182+
183+
`Pusher::trigger` will throw a `TypeError` if called with parameters of the wrong type; or a `ValueError` if called on more than 100 channels, with an event name longer than 200 characters, or with more than 10240 characters of data (post JSON serialisation).
184+
185+
##### Example
186+
187+
This call will send a message to the user with id `'123'`.
188+
189+
```python
190+
pusher_client.send_to_user( u'123', u'some_event', {u'message': u'hello worlds'})
191+
```
192+
167193
## Querying Application State
168194

169195
### Getting Information For All Channels
@@ -288,6 +314,40 @@ auth = pusher_client.authenticate(
288314
# return `auth` as a response
289315
```
290316

317+
## Authenticating User
318+
319+
#### `Pusher::authenticate_user`
320+
321+
To authenticate users on Pusher Channels on your application, you can use the authenticate_user function:
322+
323+
|Argument |Description |
324+
|:-:|:-:|
325+
|socket_id `String` | **Required**<br> The channel's socket_id, also sent to you in the POST request |
326+
|user_data `Dict` |**Required for presence channels** <br> This will be a dictionary containing the data you want associated with a user. An `"id"` key is *required* |
327+
328+
|Return Values |Description |
329+
|:-:|:-:|
330+
|response `Dict` | A dictionary to send as a response to the authentication request.|
331+
332+
For more information see:
333+
* [authenticating users](https://pusher.com/docs/channels/server_api/authenticating-users/)
334+
* [auth-signatures](https://pusher.com/docs/channels/library_auth_reference/auth-signatures/)
335+
336+
##### Example
337+
338+
###### User Authentication
339+
340+
```python
341+
auth = pusher_client.authenticate_user(
342+
socket_id=u"1234.12",
343+
user_data = {
344+
u'id': u'123',
345+
u'name': u'John Smith'
346+
}
347+
)
348+
# return `auth` as a response
349+
```
350+
291351
## Terminating user connections
292352

293353
TIn order to terminate a user's connections, the user must have been authenticated. Check the [Server user authentication docs](http://pusher.com/docs/authenticating_users) for the information on how to create a user authentication endpoint.
@@ -406,9 +466,11 @@ Feature | Supported
406466
-------------------------------------------| :-------:
407467
Trigger event on single channel | *&#10004;*
408468
Trigger event on multiple channels | *&#10004;*
469+
Trigger event to a specifc user | *&#10004;*
409470
Excluding recipients from events | *&#10004;*
410471
Authenticating private channels | *&#10004;*
411472
Authenticating presence channels | *&#10004;*
473+
Authenticating users | *&#10004;*
412474
Get the list of channels in an application | *&#10004;*
413475
Get the state of a single channel | *&#10004;*
414476
Get a list of users in a presence channel | *&#10004;*

pusher/authentication_client.py

+39-19
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
ensure_binary,
2121
validate_channel,
2222
validate_socket_id,
23+
validate_user_data,
2324
channel_name_re
24-
)
25+
)
2526

2627
from pusher.client import Client
2728
from pusher.http import GET, POST, Request, request_method
@@ -31,21 +32,21 @@
3132

3233
class AuthenticationClient(Client):
3334
def __init__(
34-
self,
35-
app_id,
36-
key,
37-
secret,
38-
ssl=True,
39-
host=None,
40-
port=None,
41-
timeout=5,
42-
cluster=None,
43-
encryption_master_key=None,
44-
encryption_master_key_base64=None,
45-
json_encoder=None,
46-
json_decoder=None,
47-
backend=None,
48-
**backend_options):
35+
self,
36+
app_id,
37+
key,
38+
secret,
39+
ssl=True,
40+
host=None,
41+
port=None,
42+
timeout=5,
43+
cluster=None,
44+
encryption_master_key=None,
45+
encryption_master_key_base64=None,
46+
json_encoder=None,
47+
json_decoder=None,
48+
backend=None,
49+
**backend_options):
4950

5051
super(AuthenticationClient, self).__init__(
5152
app_id,
@@ -63,7 +64,6 @@ def __init__(
6364
backend,
6465
**backend_options)
6566

66-
6767
def authenticate(self, channel, socket_id, custom_data=None):
6868
"""Used to generate delegated client subscription token.
6969
@@ -89,7 +89,7 @@ def authenticate(self, channel, socket_id, custom_data=None):
8989
signature = sign(self.secret, string_to_sign)
9090

9191
auth = "%s:%s" % (self.key, signature)
92-
response_payload = { "auth": auth }
92+
response_payload = {"auth": auth}
9393

9494
if is_encrypted_channel(channel):
9595
shared_secret = generate_shared_secret(
@@ -102,6 +102,25 @@ def authenticate(self, channel, socket_id, custom_data=None):
102102

103103
return response_payload
104104

105+
def authenticate_user(self, socket_id, user_data=None):
106+
"""Creates a user authentication signature.
107+
108+
:param socket_id: id of the socket that requires authorization
109+
:param user_data: used to provide user info
110+
"""
111+
validate_user_data(user_data)
112+
socket_id = validate_socket_id(socket_id)
113+
114+
user_data_encoded = json.dumps(user_data, cls=self._json_encoder)
115+
116+
string_to_sign = "%s::user::%s" % (socket_id, user_data_encoded)
117+
118+
signature = sign(self.secret, string_to_sign)
119+
120+
auth_response = "%s:%s" % (self.key, signature)
121+
response_payload = {"auth": auth_response, 'user_data': user_data_encoded}
122+
123+
return response_payload
105124

106125
def validate_webhook(self, key, signature, body):
107126
"""Used to validate incoming webhook messages. When used it guarantees
@@ -131,7 +150,8 @@ def validate_webhook(self, key, signature, body):
131150
if not time_ms:
132151
return None
133152

134-
if abs(time.time()*1000 - time_ms) > 300000:
153+
if abs(time.time() * 1000 - time_ms) > 300000:
135154
return None
136155

137156
return body_data
157+

pusher/pusher.py

+26-20
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pusher.util import (
1717
ensure_text,
1818
pusher_url_re,
19-
doc_string)
19+
doc_string, validate_user_id)
2020

2121
from pusher.pusher_client import PusherClient
2222
from pusher.authentication_client import AuthenticationClient
@@ -45,21 +45,21 @@ class Pusher(object):
4545
:param backend_options: additional backend
4646
"""
4747
def __init__(
48-
self,
49-
app_id,
50-
key,
51-
secret,
52-
ssl=True,
53-
host=None,
54-
port=None,
55-
timeout=5,
56-
cluster=None,
57-
encryption_master_key=None,
58-
encryption_master_key_base64=None,
59-
json_encoder=None,
60-
json_decoder=None,
61-
backend=None,
62-
**backend_options):
48+
self,
49+
app_id,
50+
key,
51+
secret,
52+
ssl=True,
53+
host=None,
54+
port=None,
55+
timeout=5,
56+
cluster=None,
57+
encryption_master_key=None,
58+
encryption_master_key_base64=None,
59+
json_encoder=None,
60+
json_decoder=None,
61+
backend=None,
62+
**backend_options):
6363

6464
self._pusher_client = PusherClient(
6565
app_id,
@@ -93,7 +93,6 @@ def __init__(
9393
backend,
9494
**backend_options)
9595

96-
9796
@classmethod
9897
def from_url(cls, url, **options):
9998
"""Alternative constructor that extracts the information from a URL.
@@ -123,7 +122,6 @@ def from_url(cls, url, **options):
123122

124123
return cls(**options_)
125124

126-
127125
@classmethod
128126
def from_env(cls, env='PUSHER_URL', **options):
129127
"""Alternative constructor that extracts the information from an URL
@@ -143,12 +141,16 @@ def from_env(cls, env='PUSHER_URL', **options):
143141

144142
return cls.from_url(val, **options)
145143

146-
147144
@doc_string(PusherClient.trigger.__doc__)
148145
def trigger(self, channels, event_name, data, socket_id=None):
149146
return self._pusher_client.trigger(
150147
channels, event_name, data, socket_id)
151148

149+
@doc_string(PusherClient.trigger.__doc__)
150+
def send_to_user(self, user_id, event_name, data):
151+
validate_user_id(user_id)
152+
user_server_string = "#server-to-user-%s" % user_id
153+
return self._pusher_client.trigger([user_server_string], event_name, data)
152154

153155
@doc_string(PusherClient.trigger_batch.__doc__)
154156
def trigger_batch(self, batch=[], already_encoded=False):
@@ -158,7 +160,6 @@ def trigger_batch(self, batch=[], already_encoded=False):
158160
def channels_info(self, prefix_filter=None, attributes=[]):
159161
return self._pusher_client.channels_info(prefix_filter, attributes)
160162

161-
162163
@doc_string(PusherClient.channel_info.__doc__)
163164
def channel_info(self, channel, attributes=[]):
164165
return self._pusher_client.channel_info(channel, attributes)
@@ -176,6 +177,11 @@ def authenticate(self, channel, socket_id, custom_data=None):
176177
return self._authentication_client.authenticate(
177178
channel, socket_id, custom_data)
178179

180+
@doc_string(AuthenticationClient.authenticate_user.__doc__)
181+
def authenticate_user(self, socket_id, user_data=None):
182+
return self._authentication_client.authenticate_user(
183+
socket_id, user_data
184+
)
179185

180186
@doc_string(AuthenticationClient.validate_webhook.__doc__)
181187
def validate_webhook(self, key, signature, body):

0 commit comments

Comments
 (0)