Broadcasting

Masonite comes with a powerful way to broadcast events in your application. These events can be listened to client-side using Javascript.

These can be things like a new notification which you can show on the frontend without reloading the page.

Masonite comes with one server-side driver: Pusher.

Configuration

Server Side

Server side configuration for broadcasting is done in config/broadcast.py configuration file. For now there is one driver available Pusher.

You should create an account on Pusher Channels and then create a Pusher application on your account and get the related credentials (client, app_id, secret) and the cluster location name and put this into the broadcast pusher options.

config/broadcast.py
#..

BROADCASTS = {
    "default": "pusher",
    "pusher": {
        "driver": "pusher",
        "app_id": "3456678",
        "client": "478b45309560f3456211", # key
        "secret": "ab4229346et64aa8908",
        "cluster": "eu",
        "ssl": False,
    },
}

Finally make sure you install the pusher python package

pip install pusher

Client Side

To be able to receive broadcast events in the browser you should install Javascript Pusher SDK.

Include the pusher-js script tag on your page

<script src="https://js.pusher.com/7.0.3/pusher.min.js"></script>

This is the quickest way to install Pusher. But for a real app you will often use a build system to install and compile assets and will install the Javascript Pusher SDK with npm install pusher-js and then import Pusher class with import Pusher from 'pusher-js';.

Create a Pusher instance configured with your credentials

const pusher = new Pusher("478b45309560f3456211", {
  cluster: "eu",
});

It is advised to use environment variables instead of hard-coding credentials client-side. If you're using Laravel Mix to compile assets then you should prefix your environment variables with MIX_.

Now you're ready to subscribe to channels and listen for channels events.

Creating Events

Broadcast events are simple classes that inherit from CanBroadcast. You may use any class, including the Masonite Event classes.

A broadcast event will look like this:

from masonite.broadcasting import CanBroadcast, Channel

class UserAdded(CanBroadcast):

    def broadcast_on(self):
        return Channel("channel_name")

Note that the event name emitted to the client will be the name of the class. Here it would be UserAdded.

Broadcasting Events

You can broadcast the event using the Broadcast facade or by resolving Broadcast class from container.

You can broadcast easily without creating a Broadcast event

from masonite.facades import Broadcast

Broadcast.channel('channel_name', "event_name", {"key": "value"})

Or you can broadcast the event class created earlier

from masonite.facades import Broadcast
from app.broadcasts import UserAdded

broadcast.channel(UserAdded())

You may broadcast on multiple channels as well:

Broadcast.channel(['channel1', 'channel2'], "event_name", {"key": "value"})

This type of broadcasting will emit all channels as public. For private and presence channels, keep reading.

Listening For Events

In this section we will use the client pusher instance configured earlier.

To listen for events on client-side you must first subscribe to the channel the events are emitted on

const channel = pusher.subscribe("my-channel");

Then you can listen for events

channel.bind("my-event", (data) => {
  // Method to be dispatched when event is received
});

Channel Types

Different channel types are included in Masonite.

Public Channels

Inside the event class you can specify a Public channel. These channels allow anyone with a connection to listen to events on this channel:

from masonite.broadcasting import CanBroadcast
from masonite.broadcasting import Channel

class UserAdded(CanBroadcast):

    def broadcast_on(self):
        return Channel("channel_name")

Private Channels

Private channels require authorization from the user to connect to the channel. You can use this channel to emit only events that a user should listen in on.

Private channels are channels that start with a private- prefix. When using private channels, the prefix will be prepended for you automatically.

Private channels can only be broadasting on if users are logged in. When the channel is authorized, it will check if the user is currently authenticated before it broadcasts. If the user is not authenticated it will not broadcast anything on this channel.

from masonite.broadcasting import CanBroadcast
from masonite.broadcasting import PrivateChannel

class UserAdded(CanBroadcast):

    def broadcast_on(self):
      return PrivateChannel("channel_name")

This will emit events on the private-channel_name channel.

Routing

On the frontend, when you make a connection to a private channel, a POST request is triggered by the broadcast client to authenticate the private channel. Masonite ships with this authentication route for you. All you need to do is add it to your routes:

from masonite.broadcasting import Broadcast

ROUTES = [
  # Normal route list here
]

ROUTES += Broadcast.routes()

This will create a route you can authenticate you private channel on the frontend. The authorization route will be /broadcasting/authorize but you can change this to anything you like:

ROUTES += Broadcast.routes(auth_route="/pusher/user-auth")

Pusher is expecting the authentication route to be /pusher/user-auth by default. If you want to change this client-side you can do it when creating Pusher instance

const pusher = new Pusher("478b45309560f3456211", {
  cluster: "eu",
  userAuthentication: {
    endpoint: "/broadcast/auth",
  },
});

You will also need to add the /pusher/user-auth route to the CSRF exemption.

from masonite.middleware import VerifyCsrfToken as Middleware


class VerifyCsrfToken(Middleware):

    exempt = [
        '/pusher/user-auth'
    ]

The reason for this is that the broadcast client will not send the CSRF token along with the POST authorization request.

If you want to keep CSRF protection you can read more about it here.

Authorizing

The default behaviour is to authorize everyone to access any private channels.

If you want to customize channels authorization logic you can add your own broadcast authorization route with a custom controller. Let's imagine you want to authenticate channels per users, meaning that user with ID 1 will be able to authenticate to channel private-1, user with ID 2 to channel private-2 and so on.

First you need to remove Broadcast.routes() from your routes and add your own route

# routes/web.py
from masonite.routes import Route


ROUTES = [
    Route.post("/pusher/user-auth", "BroadcastController@authorize")
    #..
]

Then you need to create a custom controller to implement your logic

# app/controllers/BroadcastController.py
from masonite.controllers import Controller
from masonite.request import Request
from masonite.broadcasting import Broadcast
from masonite.helpers import optional


class BroadcastController(Controller):

    def authorize(self, request: Request, broadcast: Broadcast):
        channel_name = request.input("channel_name")
        _, user_id = channel_name.split("-")

        if int(user_id) == optional(request.user()).id:
            return broadcast.driver("pusher").authorize(
                channel_name, request.input("socket_id")
            )
        else:
            return False

Presence Channels

Presence channels work exactly the same as private channels except you can see who else is inside this channel. This is great for chatroom type applications.

For Presence channels, the user also has to be authenticated.

from masonite.broadcasting import CanBroadcast
from masonite.broadcasting import PresenceChannel

class UserAdded(CanBroadcast):

    def broadcast_on(self):
      return PresenceChannel("channel_name")

This will emit events on the presence-channel_name channel.

Routing

Adding the authentication route is the same as for Private channels.

Authorizing

Authorizing channels is the same as for Private channels.

Examples

To get started more easily with event broadcasting in Masonite, two small examples are available here:

Sending public app releases notification

To do this we need to create a NewRelease Broadcast event and trigger this event from the backend.

# app/broadcasts/NewRelease.py
from masonite.broadcasting import CanBroadcast, Channel

class NewRelease(CanBroadcast):

    def __init__(self, version):
        self.version = version

    def broadcast_on(self):
        return Channel("releases")

    def broadcast_with(self):
        return {"message": f"Version {self.version} has been released !"}
from masonite.facades import Broadcast
from app.broadcasts import NewRelease

Broadcast.channel(NewRelease("4.0.0"))

On the frontend we need to listen to releases channel and subscribe to NewRelease events to display an alert box with the release message.

<html lang="en">
<head>
  <title>Document</title>
  <script src="https://js.pusher.com/7.0/pusher.min.js"></script>
</head>
<body>
  <script>
    const pusher = new Pusher("478b45309560f3456211", {
      cluster: "eu"
    });

    const channel = pusher.subscribe('releases');
    channel.bind('NewRelease', (data) => {
      alert(data.message)
    })
  </script>
</body>
</html>

Sending private alerts to admin users

Let's imagine our User model has two roles basic and admin and that we want to send alerts to admin users only. The basic users should not be authorized to subscribe to the alerts.

To achieve this on the backend we need to:

  • create a custom authentication route to authorize admin users only on channel private-admins

  • create a AdminUserAlert Broadcast event

  • trigger this event from the backend.

Let's first create the authentication route and controller

# routes/web.py
from masonite.routes import Route


ROUTES = [
    Route.post("/pusher/user-auth", "BroadcastController@authorize")
    #..
]
# app/controllers/BroadcastController.py
from masonite.controllers import Controller
from masonite.request import Request
from masonite.broadcasting import Broadcast
from masonite.helpers import optional


class BroadcastController(Controller):

    def authorize(self, request: Request, broadcast: Broadcast):
        channel_name = request.input("channel_name")
        authorized = True

        # check permissions for private-admins channel else authorize every other channels
        if channel_name == "private-admins":
            # check that user is logged in and admin
            if optional(request.user()).role != "admin":
                authorized = False

        if authorized:
            return broadcast.driver("pusher").authorize(
                channel_name, request.input("socket_id")
            )
        else:
            return False
# app/broadcasts/UserAlert.py
from masonite.broadcasting import CanBroadcast, Channel

class AdminUserAlert(CanBroadcast):

    def __init__(self, message, level="error"):
        self.message = message
        self.level = level

    def broadcast_on(self):
        return Channel("private-admins")

    def broadcast_with(self):
        return {"message": self.message, "level": self.level}
from masonite.facades import Broadcast
from app.broadcasts import AdminUserAlert

Broadcast.channel(AdminUserAlert("Some dependencies are outdated !", level="warning"))

On the frontend we need to listen to private-admins channel and subscribe to AdminUserAlert events to display an alert box with the message.

<html lang="en">
<head>
  <title>Document</title>
  <script src="https://js.pusher.com/7.0/pusher.min.js"></script>
</head>
<body>
  <script>
    const pusher = new Pusher("478b45309560f3456211", {
      cluster: "eu"
    });

    const channel = pusher.subscribe('private-admins');
    channel.bind('AdminUserAlert', (data) => {
      alert(`[${data.level.toUpperCase()}] ${data.message}`)
    })
  </script>
</body>
</html>

You're ready to start broadcasting events in your app !

Last updated