Skip to content

Commit 30600ba

Browse files
committed
[FEATURE-REQUEST]: Websocket via channels[daphne]
1 parent 79279af commit 30600ba

File tree

12 files changed

+222
-19
lines changed

12 files changed

+222
-19
lines changed

jaseci_serv/jaseci_serv/asgi.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,16 @@
88
"""
99

1010
import os
11-
11+
from .socket.routing import websocket_urlpatterns
1212
from django.core.asgi import get_asgi_application
13+
from channels.routing import ProtocolTypeRouter, URLRouter
14+
from channels.auth import AuthMiddlewareStack
1315

1416
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jaseci_serv.settings")
1517

16-
application = get_asgi_application()
18+
application = ProtocolTypeRouter(
19+
{
20+
"http": get_asgi_application(),
21+
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
22+
}
23+
)

jaseci_serv/jaseci_serv/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
# Application definition
3838

3939
INSTALLED_APPS = [
40+
"daphne",
4041
"django.contrib.admin",
4142
"django.contrib.auth",
4243
"django.contrib.contenttypes",
@@ -96,7 +97,7 @@
9697
},
9798
]
9899

99-
WSGI_APPLICATION = "jaseci_serv.wsgi.application"
100+
ASGI_APPLICATION = "jaseci_serv.asgi.application"
100101

101102
# Database
102103
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# **HOW TO SETUP `CHANNEL_LAYER`**
2+
3+
## **GLOBAL VARS:** `CHANNEL_LAYER`
4+
### **`IN MEMORY`** *(default)*
5+
```json
6+
{
7+
"BACKEND": "channels.layers.InMemoryChannelLayer"
8+
}
9+
```
10+
- This config will only works with single jaseci instance. Notification from async walkers will also not work
11+
12+
### **`REDIS`**
13+
```json
14+
{
15+
"BACKEND": "channels_redis.core.RedisChannelLayer",
16+
"CONFIG": {
17+
"hosts": [
18+
["localhost", 6379]
19+
]
20+
}
21+
}
22+
```
23+
- This should work on mutiple jaseci instance. Notification from async walker should also work
24+
---
25+
# **`NOTIFICATION FROM JAC`**
26+
## wb.**`notify`**
27+
> **`Arguments`:** \
28+
> **target**: str \
29+
> **data**: dict
30+
>
31+
> **`Return`:** \
32+
> None
33+
>
34+
> **`Usage`:** \
35+
> Send notification from jac to target group
36+
>
37+
> **`Remarks`:** \
38+
> if user is logged in, `target` can be master id without `urn:uuid:` \
39+
> else used thed `session_id` from client connection
40+
##### **`HOW TO TRIGGER`**
41+
```js
42+
wb.notify(target, {"test": 123456});
43+
```

jaseci_serv/jaseci_serv/socket/__init__.py

Whitespace-only changes.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import os
2+
from uuid import uuid4
3+
from json import loads, dumps
4+
5+
from asgiref.sync import async_to_sync
6+
from channels.layers import settings
7+
from channels.generic.websocket import WebsocketConsumer
8+
9+
from .event_action import authenticated_user
10+
from jaseci_serv.base.models import lookup_global_config
11+
12+
13+
class SocketConsumer(WebsocketConsumer):
14+
def connect(self):
15+
self.accept()
16+
session_id = None
17+
authenticated = False
18+
target = self.scope["url_route"]["kwargs"]["target"]
19+
20+
if target == "anonymous":
21+
self.target = session_id = str(uuid4())
22+
else:
23+
user = authenticated_user(target)
24+
if user:
25+
self.target = user.master.urn[9:]
26+
authenticated = True
27+
else:
28+
self.target = session_id = str(uuid4())
29+
30+
async_to_sync(self.channel_layer.group_add)(self.target, self.channel_name)
31+
self.send(
32+
text_data=dumps(
33+
{
34+
"type": "connect",
35+
"authenticated": authenticated,
36+
"session_id": session_id,
37+
}
38+
)
39+
)
40+
41+
def receive(self, text_data=None, bytes_data=None):
42+
data = loads(text_data)
43+
44+
async_to_sync(self.channel_layer.group_send)(
45+
self.target, {"type": "notify", "data": data}
46+
)
47+
48+
def notify(self, data):
49+
self.send(text_data=dumps(data))
50+
51+
52+
setattr(
53+
settings,
54+
"CHANNEL_LAYERS",
55+
{
56+
"default": loads(
57+
lookup_global_config(
58+
"CHANNEL_LAYER", '{"BACKEND": "channels.layers.InMemoryChannelLayer"}'
59+
)
60+
)
61+
},
62+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
try:
2+
from hmac import compare_digest
3+
except ImportError:
4+
5+
def compare_digest(a, b):
6+
return a == b
7+
8+
9+
import binascii
10+
from knox.crypto import hash_token
11+
from knox.models import AuthToken
12+
from knox.settings import CONSTANTS
13+
from jaseci.jsorc.live_actions import jaseci_action
14+
from channels.layers import get_channel_layer
15+
from asgiref.sync import async_to_sync
16+
from django.utils import timezone
17+
18+
19+
def authenticated_user(token: str):
20+
for auth_token in AuthToken.objects.filter(
21+
token_key=token[: CONSTANTS.TOKEN_KEY_LENGTH]
22+
):
23+
try:
24+
digest = hash_token(token)
25+
if (
26+
compare_digest(digest, auth_token.digest)
27+
and auth_token.expiry > timezone.now()
28+
):
29+
return auth_token.user
30+
except (TypeError, binascii.Error):
31+
pass
32+
return None
33+
34+
35+
@jaseci_action(act_group=["wb"])
36+
def notify(target: str, data: dict):
37+
async_to_sync(get_channel_layer().group_send)(
38+
target, {"type": "notify", "data": data}
39+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.urls import path
2+
from . import consumer
3+
4+
websocket_urlpatterns = [
5+
path(r"ws/socket-server/<str:target>", consumer.SocketConsumer.as_asgi())
6+
]

jaseci_serv/jaseci_serv/socket/tests/__init__.py

Whitespace-only changes.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import pytest
2+
from uuid import UUID
3+
from json import loads
4+
from jaseci.utils.utils import TestCaseHelper
5+
from django.test import TestCase
6+
7+
from channels.routing import URLRouter
8+
from channels.testing import WebsocketCommunicator
9+
10+
11+
class WebSocketTests(TestCaseHelper, TestCase):
12+
"""Test the publicly available node API"""
13+
14+
def setUp(self):
15+
super().setUp()
16+
17+
from ..routing import websocket_urlpatterns
18+
19+
self.app = URLRouter(websocket_urlpatterns)
20+
21+
def tearDown(self):
22+
super().tearDown()
23+
24+
def is_valid_uuid(self, uuid):
25+
try:
26+
_uuid = UUID(uuid, version=4)
27+
except ValueError:
28+
return False
29+
return str(_uuid) == uuid
30+
31+
@pytest.mark.asyncio
32+
async def test_websocket(self):
33+
communicator = WebsocketCommunicator(self.app, "ws/socket-server/anonymous")
34+
35+
connected, subprotocol = await communicator.connect()
36+
self.assertTrue(connected)
37+
response: dict = loads(await communicator.receive_from())
38+
self.assertTrue(self.is_valid_uuid(response.pop("session_id")))
39+
self.assertEqual(response, {"type": "connect", "authenticated": False})
40+
41+
await communicator.send_to(text_data='{"test": true}')
42+
response: dict = loads(await communicator.receive_from())
43+
self.assertEqual(response, {"type": "notify", "data": {"test": True}})
44+
45+
await communicator.disconnect()

jaseci_serv/jaseci_serv/wsgi.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

jaseci_serv/setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def get_ver():
4848
"dj-rest-auth[with_social]",
4949
"django-allauth>=0.52.0",
5050
"tzdata>=2022.7",
51+
"channels[daphne]==4.0.0",
52+
"channels-redis",
5153
],
5254
package_data={
5355
"": [

jaseci_serv/templates/examples/social_auth.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,18 @@ <h1>Google Identity Services Authorization Token model</h1>
145145
});
146146
{% endif %}
147147
{% endif %}
148+
149+
{% if provider == "google" %}
150+
socket = new WebSocket(`ws://${window.location.host}/ws/socket-server/276a40aec1dffc48a25463c3e2545473b45a663364adf3a2f523b903aa254c9f`)
151+
{% else %}
152+
socket = new WebSocket(`ws://${window.location.host}/ws/socket-server/anonymous`)
153+
{% endif %}
154+
socket.onmessage = (event) => {
155+
console.log(event)
156+
}
157+
158+
function notify-server() {
159+
socket.send(JSON.stringify({"message": "test"}))
160+
}
161+
148162
</script>

0 commit comments

Comments
 (0)