diff --git a/channels/adapters.py b/channels/adapters.py index cf365cd..add1339 100644 --- a/channels/adapters.py +++ b/channels/adapters.py @@ -3,7 +3,7 @@ import functools from django.core.handlers.base import BaseHandler from django.http import HttpRequest, HttpResponse -from channels import Channel, channel_backends, DEFAULT_CHANNEL_BACKEND +from channels import Channel class UrlConsumer(object): @@ -15,13 +15,13 @@ class UrlConsumer(object): self.handler = BaseHandler() self.handler.load_middleware() - def __call__(self, channel, **kwargs): - request = HttpRequest.channel_decode(kwargs) + def __call__(self, message): + request = HttpRequest.channel_decode(message.content) try: response = self.handler.get_response(request) except HttpResponse.ResponseLater: return - Channel(request.response_channel).send(**response.channel_encode()) + message.reply_channel.send(response.channel_encode()) def view_producer(channel_name): @@ -30,24 +30,19 @@ def view_producer(channel_name): and abandons the response (with an exception the Worker will catch) """ def producing_view(request): - Channel(channel_name).send(**request.channel_encode()) + Channel(channel_name).send(request.channel_encode()) raise HttpResponse.ResponseLater() return producing_view -def view_consumer(channel_name, alias=DEFAULT_CHANNEL_BACKEND): +def view_consumer(func): """ Decorates a normal Django view to be a channel consumer. Does not run any middleware """ - def inner(func): - @functools.wraps(func) - def consumer(channel, **kwargs): - request = HttpRequest.channel_decode(kwargs) - response = func(request) - Channel(request.response_channel).send(**response.channel_encode()) - # Get the channel layer and register - channel_backend = channel_backends[DEFAULT_CHANNEL_BACKEND] - channel_backend.registry.add_consumer(consumer, [channel_name]) - return func - return inner + @functools.wraps(func) + def consumer(message): + request = HttpRequest.channel_decode(message.content) + response = func(request) + message.reply_channel.send(response.channel_encode()) + return func diff --git a/channels/channel.py b/channels/channel.py index faf5f3a..0e047b5 100644 --- a/channels/channel.py +++ b/channels/channel.py @@ -26,11 +26,13 @@ class Channel(object): else: self.channel_backend = channel_backends[alias] - def send(self, **kwargs): + def send(self, content): """ - Send a message over the channel, taken from the kwargs. + Send a message over the channel - messages are always dicts. """ - self.channel_backend.send(self.name, kwargs) + if not isinstance(content, dict): + raise ValueError("You can only send dicts as content on channels.") + self.channel_backend.send(self.name, content) @classmethod def new_name(self, prefix): @@ -51,6 +53,9 @@ class Channel(object): from channels.adapters import view_producer return view_producer(self.name) + def __str__(self): + return self.name + class Group(object): """ @@ -66,13 +71,19 @@ class Group(object): self.channel_backend = channel_backends[alias] def add(self, channel): + if isinstance(channel, Channel): + channel = channel.name self.channel_backend.group_add(self.name, channel) def discard(self, channel): + if isinstance(channel, Channel): + channel = channel.name self.channel_backend.group_discard(self.name, channel) def channels(self): self.channel_backend.group_channels(self.name) - def send(self, **kwargs): - self.channel_backend.send_group(self.name, kwargs) + def send(self, content): + if not isinstance(content, dict): + raise ValueError("You can only send dicts as content on channels.") + self.channel_backend.send_group(self.name, content) diff --git a/channels/decorators.py b/channels/decorators.py index bd70058..bc5f116 100644 --- a/channels/decorators.py +++ b/channels/decorators.py @@ -25,27 +25,27 @@ def http_session(func): be None, rather than an empty session you can write to. """ @functools.wraps(func) - def inner(*args, **kwargs): - if "COOKIES" not in kwargs and "GET" not in kwargs: + def inner(message, *args, **kwargs): + if "COOKIES" not in message.content and "GET" not in message.content: raise ValueError("No COOKIES or GET sent to consumer; this decorator can only be used on messages containing at least one.") # Make sure there's a session key session_key = None - if "GET" in kwargs: + if "GET" in message.content: try: - session_key = kwargs['GET'].get("session_key", [])[0] + session_key = message.content['GET'].get("session_key", [])[0] except IndexError: pass - if "COOKIES" in kwargs and session_key is None: - session_key = kwargs['COOKIES'].get(settings.SESSION_COOKIE_NAME) + if "COOKIES" in message.content and session_key is None: + session_key = message.content['COOKIES'].get(settings.SESSION_COOKIE_NAME) # Make a session storage if session_key: session_engine = import_module(settings.SESSION_ENGINE) session = session_engine.SessionStore(session_key=session_key) else: session = None - kwargs['session'] = session + message.session = session # Run the consumer - result = func(*args, **kwargs) + result = func(message, *args, **kwargs) # Persist session if needed (won't be saved if error happens) if session is not None and session.modified: session.save() @@ -65,46 +65,49 @@ def http_django_auth(func): """ @http_session @functools.wraps(func) - def inner(*args, **kwargs): + def inner(message, *args, **kwargs): # If we didn't get a session, then we don't get a user - if kwargs['session'] is None: - kwargs['user'] = None + if not hasattr(message, "session"): + raise ValueError("Did not see a session to get auth from") + if message.session is None: + message.user = None # Otherwise, be a bit naughty and make a fake Request with just # a "session" attribute (later on, perhaps refactor contrib.auth to # pass around session rather than request) else: - fake_request = type("FakeRequest", (object, ), {"session": kwargs['session']}) - kwargs['user'] = auth.get_user(fake_request) + fake_request = type("FakeRequest", (object, ), {"session": message.session}) + message.user = auth.get_user(fake_request) # Run the consumer - return func(*args, **kwargs) + return func(message, *args, **kwargs) return inner -def send_channel_session(func): +def channel_session(func): """ Provides a session-like object called "channel_session" to consumers as a message attribute that will auto-persist across consumers with - the same incoming "send_channel" value. + the same incoming "reply_channel" value. """ @functools.wraps(func) - def inner(*args, **kwargs): - # Make sure there's a send_channel in kwargs - if "send_channel" not in kwargs: - raise ValueError("No send_channel sent to consumer; this decorator can only be used on messages containing it.") - # Turn the send_channel into a valid session key length thing. + def inner(message, *args, **kwargs): + # Make sure there's a reply_channel in kwargs + if not message.reply_channel: + raise ValueError("No reply_channel sent to consumer; this decorator can only be used on messages containing it.") + # Turn the reply_channel into a valid session key length thing. # We take the last 24 bytes verbatim, as these are the random section, # and then hash the remaining ones onto the start, and add a prefix # TODO: See if there's a better way of doing this - session_key = "skt" + hashlib.md5(kwargs['send_channel'][:-24]).hexdigest()[:8] + kwargs['send_channel'][-24:] + reply_name = message.reply_channel.name + session_key = "skt" + hashlib.md5(reply_name[:-24]).hexdigest()[:8] + reply_name[-24:] # Make a session storage session_engine = import_module(settings.SESSION_ENGINE) session = session_engine.SessionStore(session_key=session_key) # If the session does not already exist, save to force our session key to be valid if not session.exists(session.session_key): session.save() - kwargs['channel_session'] = session + message.channel_session = session # Run the consumer - result = func(*args, **kwargs) + result = func(message, *args, **kwargs) # Persist session if needed (won't be saved if error happens) if session.modified: session.save() diff --git a/channels/interfaces/websocket_twisted.py b/channels/interfaces/websocket_twisted.py index e378b61..8aa0b31 100644 --- a/channels/interfaces/websocket_twisted.py +++ b/channels/interfaces/websocket_twisted.py @@ -23,30 +23,26 @@ class InterfaceProtocol(WebSocketServerProtocol): def onOpen(self): # Make sending channel - self.send_channel = Channel.new_name("!django.websocket.send") + self.reply_channel = Channel.new_name("!django.websocket.send") + self.request_info["reply_channel"] = self.reply_channel self.last_keepalive = time.time() - self.factory.protocols[self.send_channel] = self + self.factory.protocols[self.reply_channel] = self # Send news that this channel is open - Channel("django.websocket.connect").send( - send_channel = self.send_channel, - **self.request_info - ) + Channel("django.websocket.connect").send(self.request_info) def onMessage(self, payload, isBinary): if isBinary: - Channel("django.websocket.receive").send( - send_channel = self.send_channel, + Channel("django.websocket.receive").send(dict( + self.request_info, content = payload, binary = True, - **self.request_info - ) + )) else: - Channel("django.websocket.receive").send( - send_channel = self.send_channel, + Channel("django.websocket.receive").send(dict( + self.request_info, content = payload.decode("utf8"), binary = False, - **self.request_info - ) + )) def serverSend(self, content, binary=False, **kwargs): """ @@ -64,21 +60,15 @@ class InterfaceProtocol(WebSocketServerProtocol): self.sendClose() def onClose(self, wasClean, code, reason): - if hasattr(self, "send_channel"): - del self.factory.protocols[self.send_channel] - Channel("django.websocket.disconnect").send( - send_channel = self.send_channel, - **self.request_info - ) + if hasattr(self, "reply_channel"): + del self.factory.protocols[self.reply_channel] + Channel("django.websocket.disconnect").send(self.request_info) def sendKeepalive(self): """ Sends a keepalive packet on the keepalive channel. """ - Channel("django.websocket.keepalive").send( - send_channel = self.send_channel, - **self.request_info - ) + Channel("django.websocket.keepalive").send(self.request_info) self.last_keepalive = time.time() @@ -94,7 +84,7 @@ class InterfaceFactory(WebSocketServerFactory): super(InterfaceFactory, self).__init__(*args, **kwargs) self.protocols = {} - def send_channels(self): + def reply_channels(self): return self.protocols.keys() def dispatch_send(self, channel, message): @@ -128,7 +118,7 @@ class WebsocketTwistedInterface(object): Run in a separate thread; reads messages from the backend. """ while True: - channels = self.factory.send_channels() + channels = self.factory.reply_channels() # Quit if reactor is stopping if not reactor.running: return diff --git a/channels/interfaces/wsgi.py b/channels/interfaces/wsgi.py index 60b19c2..8df49c0 100644 --- a/channels/interfaces/wsgi.py +++ b/channels/interfaces/wsgi.py @@ -15,7 +15,7 @@ class WSGIInterface(WSGIHandler): super(WSGIInterface, self).__init__(*args, **kwargs) def get_response(self, request): - request.response_channel = Channel.new_name("django.wsgi.response") - Channel("django.wsgi.request", channel_backend=self.channel_backend).send(**request.channel_encode()) - channel, message = self.channel_backend.receive_many_blocking([request.response_channel]) + request.reply_channel = Channel.new_name("django.wsgi.response") + Channel("django.wsgi.request", channel_backend=self.channel_backend).send(request.channel_encode()) + channel, message = self.channel_backend.receive_many_blocking([request.reply_channel]) return HttpResponse.channel_decode(message) diff --git a/channels/message.py b/channels/message.py new file mode 100644 index 0000000..bc7eda5 --- /dev/null +++ b/channels/message.py @@ -0,0 +1,18 @@ +from .channel import Channel + + +class Message(object): + """ + Represents a message sent over a Channel. + + The message content is a dict called .content, while + reply_channel is an optional extra attribute representing a channel + to use to reply to this message's end user, if that makes sense. + """ + + def __init__(self, content, channel, channel_backend, reply_channel=None): + self.content = content + self.channel = channel + self.channel_backend = channel_backend + if reply_channel: + self.reply_channel = Channel(reply_channel, channel_backend=self.channel_backend) diff --git a/channels/request.py b/channels/request.py index 7e5a71e..dafcf49 100644 --- a/channels/request.py +++ b/channels/request.py @@ -17,7 +17,7 @@ def encode_request(request): "path": request.path, "path_info": request.path_info, "method": request.method, - "response_channel": request.response_channel, + "reply_channel": request.reply_channel, } return value @@ -34,7 +34,7 @@ def decode_request(value): request.path = value['path'] request.method = value['method'] request.path_info = value['path_info'] - request.response_channel = value['response_channel'] + request.reply_channel = value['reply_channel'] return request diff --git a/channels/worker.py b/channels/worker.py index 1e7f2ea..db0a896 100644 --- a/channels/worker.py +++ b/channels/worker.py @@ -1,4 +1,5 @@ import traceback +from .message import Message class Worker(object): @@ -17,12 +18,18 @@ class Worker(object): """ channels = self.channel_backend.registry.all_channel_names() while True: - channel, message = self.channel_backend.receive_many_blocking(channels) + channel, content = self.channel_backend.receive_many_blocking(channels) + message = Message( + content=content, + channel=channel, + channel_backend=self.channel_backend, + reply_channel=content.get("reply_channel", None), + ) # Handle the message consumer = self.channel_backend.registry.consumer_for_channel(channel) if self.callback: self.callback(channel, message) try: - consumer(channel=channel, **message) + consumer(message) except: traceback.print_exc() diff --git a/docs/concepts.rst b/docs/concepts.rst index 4c16092..d331b37 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -100,17 +100,17 @@ message and can write out zero to many other channel messages. Now, let's make a channel for requests (called ``django.wsgi.request``), and a channel per client for responses (e.g. ``django.wsgi.response.o4F2h2Fd``), -with the response channel a property (``send_channel``) of the request message. +with the response channel a property (``reply_channel``) of the request message. Suddenly, a view is merely another example of a consumer:: @consumer("django.wsgi.request") - def my_consumer(send_channel, **request_data): + def my_consumer(reply_channel, **request_data): # Decode the request from JSON-compat to a full object django_request = Request.decode(request_data) # Run view django_response = view(django_request) # Encode the response into JSON-compat format - Channel(send_channel).send(django_response.encode()) + Channel(reply_channel).send(django_response.encode()) In fact, this is how Channels works. The interface servers transform connections from the outside world (HTTP, WebSockets, etc.) into messages on channels, @@ -177,16 +177,16 @@ set of channels (here, using Redis) to send updates to:: @receiver(post_save, sender=BlogUpdate) def send_update(sender, instance, **kwargs): # Loop through all response channels and send the update - for send_channel in redis_conn.smembers("readers"): - Channel(send_channel).send( + for reply_channel in redis_conn.smembers("readers"): + Channel(reply_channel).send( id=instance.id, content=instance.content, ) @consumer("django.websocket.connect") - def ws_connect(path, send_channel, **kwargs): + def ws_connect(path, reply_channel, **kwargs): # Add to reader set - redis_conn.sadd("readers", send_channel) + redis_conn.sadd("readers", reply_channel) While this will work, there's a small problem - we never remove people from the ``readers`` set when they disconnect. We could add a consumer that @@ -221,9 +221,9 @@ we don't need to; Channels has it built in, as a feature called Groups:: @consumer("django.websocket.connect") @consumer("django.websocket.keepalive") - def ws_connect(path, send_channel, **kwargs): + def ws_connect(path, reply_channel, **kwargs): # Add to reader group - Group("liveblog").add(send_channel) + Group("liveblog").add(reply_channel) Not only do groups have their own ``send()`` method (which backends can provide an efficient implementation of), they also automatically manage expiry of diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 9a79b80..8d906e8 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -26,16 +26,16 @@ Make a new project, a new app, and put this in a ``consumers.py`` file in the ap from channels import Channel from django.http import HttpResponse - def http_consumer(response_channel, path, **kwargs): - response = HttpResponse("Hello world! You asked for %s" % path) - Channel(response_channel).send(**response.channel_encode()) + def http_consumer(message): + response = HttpResponse("Hello world! You asked for %s" % message.content['path']) + message.reply_channel.send(response.channel_encode()) The most important thing to note here is that, because things we send in messages must be JSON-serialisable, the request and response messages are in a key-value format. There are ``channel_decode()`` and ``channel_encode()`` methods on both Django's request and response classes, -but here we just take two of the request variables directly as keyword -arguments for simplicity. +but here we just use the message's ``content`` attribute directly for simplicity +(message content is always a dict). Now, go into your ``settings.py`` file, and set up a channel backend; by default, Django will just use a local backend and route HTTP requests to the normal @@ -72,8 +72,8 @@ do any time. Let's try some WebSockets, and make a basic chat server! Delete that consumer and its routing - we'll want the normal Django view layer to serve HTTP requests from now on - and make this WebSocket consumer instead:: - def ws_add(channel, send_channel, **kwargs): - Group("chat").add(send_channel) + def ws_add(message): + Group("chat").add(message.reply_channel) Hook it up to the ``django.websocket.connect`` channel like this:: @@ -90,7 +90,7 @@ Now, let's look at what this is doing. It's tied to the ``django.websocket.connect`` channel, which means that it'll get a message whenever a new WebSocket connection is opened by a client. -When it gets that message, it takes the ``send_channel`` key from it, which +When it gets that message, it takes the ``reply_channel`` attribute from it, which is the unique response channel for that client, and adds it to the ``chat`` group, which means we can send messages to all connected chat clients. @@ -107,8 +107,8 @@ a group it's already in - similarly, it's safe to discard a channel from a group it's not in):: # Connected to django.websocket.keepalive - def ws_keepalive(channel, send_channel, **kwargs): - Group("chat").add(send_channel) + def ws_keepalive(message): + Group("chat").add(message.reply_channel) Of course, this is exactly the same code as the ``connect`` handler, so let's just route both channels to the same consumer:: @@ -125,8 +125,8 @@ handler to clean up as people disconnect (most channels will cleanly disconnect and get this called):: # Connected to django.websocket.disconnect - def ws_disconnect(channel, send_channel, **kwargs): - Group("chat").discard(send_channel) + def ws_disconnect(message): + Group("chat").discard(message.reply_channel) Now, that's taken care of adding and removing WebSocket send channels for the ``chat`` group; all we need to do now is take care of message sending. For now, @@ -136,16 +136,16 @@ any message sent in to all connected clients. Here's all the code:: from channels import Channel, Group # Connected to django.websocket.connect and django.websocket.keepalive - def ws_add(channel, send_channel, **kwargs): - Group("chat").add(send_channel) + def ws_add(message): + Group("chat").add(message.reply_channel) # Connected to django.websocket.receive - def ws_message(channel, send_channel, content, **kwargs): - Group("chat").send(content=content) + def ws_message(message): + Group("chat").send(message.content) # Connected to django.websocket.disconnect - def ws_disconnect(channel, send_channel, **kwargs): - Group("chat").discard(send_channel) + def ws_disconnect(message): + Group("chat").discard(message.reply_channel) And what our routing should look like in ``settings.py``:: @@ -264,7 +264,7 @@ is much more portable, and works in development where you need to run a separate WebSocket server (by default, on port 9000). All we need to do is add the ``django_http_auth`` decorator to our views, -and we'll get extra ``session`` and ``user`` keyword arguments we can use; +and we'll get extra ``session`` and ``user`` keyword attributes on ``message`` we can use; let's make one where users can only chat to people with the same first letter of their username:: @@ -272,16 +272,16 @@ of their username:: from channels.decorators import django_http_auth @django_http_auth - def ws_add(channel, send_channel, user, **kwargs): - Group("chat-%s" % user.username[0]).add(send_channel) + def ws_add(message): + Group("chat-%s" % message.user.username[0]).add(message.reply_channel) @django_http_auth - def ws_message(channel, send_channel, content, user, **kwargs): - Group("chat-%s" % user.username[0]).send(content=content) + def ws_message(message): + Group("chat-%s" % message.user.username[0]).send(message.content) @django_http_auth - def ws_disconnect(channel, send_channel, user, **kwargs): - Group("chat-%s" % user.username[0]).discard(send_channel) + def ws_disconnect(message): + Group("chat-%s" % message.user.username[0]).discard(message.reply_channel) Now, when we connect to the WebSocket we'll have to remember to provide the Django session ID as part of the URL, like this:: @@ -293,10 +293,6 @@ Note that Channels can't work with signed cookie sessions - since only HTTP responses can set cookies, it needs a backend it can write to separately to store state. -(Also note that we always end consumers with ``**kwargs``; this is to save us -from writing out all variables we might get sent and to allow forwards-compatibility -with any additions to the message formats in the future.) - Persisting Data --------------- @@ -307,7 +303,7 @@ should let them send this request in the initial WebSocket connection, check they're allowed to access it, and then remember which room a socket is connected to when they send a message in so we know which group to send it to. -The ``send_channel`` is our unique pointer to the open WebSocket - as you've +The ``reply_channel`` is our unique pointer to the open WebSocket - as you've seen, we do all our operations on it - but it's not something we can annotate with data; it's just a simple string, and even if we hack around and set attributes on it that's not going to carry over to other workers. @@ -315,18 +311,18 @@ attributes on it that's not going to carry over to other workers. Instead, the solution is to persist information keyed by the send channel in some other data store - sound familiar? This is what Django's session framework does for HTTP requests, only there it uses cookies as the lookup key rather -than the ``send_channel``. +than the ``reply_channel``. Now, as you saw above, you can use the ``django_http_auth`` decorator to get -both a ``user`` and a ``session`` variable in your message arguments - and, -indeed, there is a ``websocket_session`` decorator that will just give you +both a ``user`` and a ``session`` attribute on your message - and, +indeed, there is a ``http_session`` decorator that will just give you the ``session`` attribute. However, that session is based on cookies, and so follows the user round the site - it's great for information that should persist across all WebSocket and HTTP connections, but not great for information that is specific to a single WebSocket (such as "which chatroom should this socket be connected to"). For -this reason, Channels also provides a ``send_channel_session`` decorator, +this reason, Channels also provides a ``channel_session`` decorator, which adds a ``channel_session`` attribute to the message; this works just like the normal ``session`` attribute, and persists to the same storage, but varies per-channel rather than per-cookie. @@ -335,31 +331,31 @@ Let's use it now to build a chat server that expects you to pass a chatroom name in the path of your WebSocket request (we'll ignore auth for now):: from channels import Channel - from channels.decorators import consumer, send_channel_session + from channels.decorators import consumer, channel_session @consumer("django.websocket.connect") - @send_channel_session - def ws_connect(channel, send_channel, path, channel_session, **kwargs): + @channel_session + def ws_connect(message): # Work out room name from path (ignore slashes) - room = path.strip("/") + room = message.content['path'].strip("/") # Save room in session and add us to the group - channel_session['room'] = room - Group("chat-%s" % room).add(send_channel) + message.channel_session['room'] = room + Group("chat-%s" % room).add(message.reply_channel) @consumer("django.websocket.keepalive") - @send_channel_session - def ws_add(channel, send_channel, channel_session, **kwargs): - Group("chat-%s" % channel_session['room']).add(send_channel) + @channel_session + def ws_add(message): + Group("chat-%s" % message.channel_session['room']).add(message.reply_channel) @consumer("django.websocket.receive") - @send_channel_session - def ws_message(channel, send_channel, content, channel_session, **kwargs): - Group("chat-%s" % channel_session['room']).send(content=content) + @channel_session + def ws_message(message): + Group("chat-%s" % message.channel_session['room']).send(content) @consumer("django.websocket.disconnect") - @send_channel_session - def ws_disconnect(channel, send_channel, channel_session, **kwargs): - Group("chat-%s" % channel_session['room']).discard(send_channel) + @channel_session + def ws_disconnect(message): + Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel) If you play around with it from the console (or start building a simple JavaScript chat client that appends received messages to a div), you'll see @@ -391,40 +387,48 @@ Let's see what that looks like, assuming we have a ChatMessage model with ``message`` and ``room`` fields:: from channels import Channel - from channels.decorators import consumer, send_channel_session + from channels.decorators import consumer, channel_session from .models import ChatMessage @consumer("chat-messages") - def msg_consumer(channel, room, message): + def msg_consumer(message): # Save to model - ChatMessage.objects.create(room=room, message=message) + ChatMessage.objects.create( + room=message.content['room'], + message=message.content['message'], + ) # Broadcast to listening sockets - Group("chat-%s" % room).send(message) + Group("chat-%s" % room).send({ + "content": message.content['message'], + }) @consumer("django.websocket.connect") - @send_channel_session - def ws_connect(channel, send_channel, path, channel_session, **kwargs): + @channel_session + def ws_connect(message): # Work out room name from path (ignore slashes) - room = path.strip("/") + room = message.content['path'].strip("/") # Save room in session and add us to the group - channel_session['room'] = room - Group("chat-%s" % room).add(send_channel) + message.channel_session['room'] = room + Group("chat-%s" % room).add(message.reply_channel) @consumer("django.websocket.keepalive") - @send_channel_session - def ws_add(channel, send_channel, channel_session, **kwargs): - Group("chat-%s" % channel_session['room']).add(send_channel) + @channel_session + def ws_add(message): + Group("chat-%s" % message.channel_session['room']).add(message.reply_channel) @consumer("django.websocket.receive") - @send_channel_session - def ws_message(channel, send_channel, content, channel_session, **kwargs): + @channel_session + def ws_message(message): # Stick the message onto the processing queue - Channel("chat-messages").send(room=channel_session['room'], message=content) + Channel("chat-messages").send({ + "room": channel_session['room'], + "message": content, + }) @consumer("django.websocket.disconnect") - @send_channel_session - def ws_disconnect(channel, send_channel, channel_session, **kwargs): - Group("chat-%s" % channel_session['room']).discard(send_channel) + @channel_session + def ws_disconnect(message): + Group("chat-%s" % message.channel_session['room']).discard(message.reply_channel) Note that we could add messages onto the ``chat-messages`` channel from anywhere; inside a View, inside another model's ``post_save`` signal, inside a management diff --git a/docs/message-standards.rst b/docs/message-standards.rst index c1c6528..c018c39 100644 --- a/docs/message-standards.rst +++ b/docs/message-standards.rst @@ -62,7 +62,7 @@ Contains the following keys: * META: Same as ``request.META`` * path: Same as ``request.path`` * path_info: Same as ``request.path_info`` -* send_channel: Channel name to send responses on +* reply_channel: Channel name to send responses on WebSocket Receive @@ -81,7 +81,7 @@ WebSocket Client Close Sent when the WebSocket is closed by either the client or the server. -Contains the same keys as WebSocket Connection, including send_channel, +Contains the same keys as WebSocket Connection, including reply_channel, though nothing should be sent on it.