aboutsummaryrefslogtreecommitdiffstats
path: root/pysite/websockets.py
blob: 213daace312a73a0da9d5358a4496939132011f3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import json

from flask import Blueprint
from geventwebsocket.websocket import WebSocket


class WS:
    """
    Base class for representing a Websocket.

    At minimum, you must implement the `on_message(self, message)` function. Without it, you won't be able to handle
    any messages, and an error will be thrown!

    If you need access to the database, you can mix-in DBMixin, just like any view class:

    >>> class DBWebsocket(WS, DBMixin):
    ...     name = "db_websocket"
    ...     path = "/db_websocket"  # This will be prefixed with "/ws" by the blueprint
    ...     table = "ws"
    ...
    ...     def on_message(self, message):
    ...         self.send(
    ...             json.loads(self.db.get(self.table_name, message))
    ...         )

    Please note that an instance of this class is created for every websocket connected to the path. This does, however,
    mean that you can store any state required by your websocket.
    """

    path = ""  # type: str
    name = ""  # type: str

    _connections = None

    def __init__(self, socket: WebSocket):
        self.socket = socket

    def __new__(cls, *args, **kwargs):
        if cls._connections is None:
            cls._connections = []

        return super().__new__(cls)

    def on_open(self):
        """
        Called once when the websocket is opened. Optional.
        """

    def on_message(self, message: str):
        """
        Called when a message is received by the websocket.
        """

        raise NotImplementedError()

    def on_close(self):
        """
        Called once when the websocket is closed. Optional.
        """

    def send(self, message, binary=None):
        """
        Send a message to the currently-connected websocket, if it's open.

        Nothing will happen if the websocket is closed.
        """

        if not self.socket.closed:
            self.socket.send(message, binary=binary)

    def send_json(self, data):
        return self.send(json.dumps(data))

    @classmethod
    def send_all(cls, message, binary=None):
        for connection in cls._connections:
            connection.send(message, binary=binary)

    @classmethod
    def send_all_json(cls, data):
        for connection in cls._connections:
            connection.send_json(data)

    @classmethod
    def setup(cls: "type(WS)", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint):
        """
        Set up the websocket object, calling `setup()` on any superclasses as necessary (for example, on the DB
        mixin).

        This function will set up a websocket handler so that it behaves in a class-oriented way. It's up to you to
        deal with message handling yourself, however.
        """

        if hasattr(super(), "setup"):
            super().setup(manager, blueprint)

        if not cls.path or not cls.name:
            raise RuntimeError("Websockets must have both `path` and `name` defined")

        cls.manager = manager

        def handle(socket: WebSocket):
            """
            Wrap the current WS class, dispatching events to it as necessary. We're using gevent, so there's
            no need to worry about blocking here.
            """

            ws = cls(socket)  # Instantiate the current class, passing it the WS object
            cls._connections.append(ws)
            try:
                ws.on_open()  # Call the "on_open" handler

                while not socket.closed:  # As long as the socket is open...
                    message = socket.receive()  # Wait for a message

                    if not socket.closed:  # If the socket didn't just close (there's always a None message on closing)
                        ws.on_message(message)  # Call the "on_message" handler

                ws.on_close()  # The socket just closed, call the "on_close" handler
            finally:
                cls._connections.remove(ws)

        blueprint.route(cls.path)(handle)  # Register the handling function to the WS blueprint