aboutsummaryrefslogtreecommitdiffstats
path: root/pysite/mixins.py
blob: 98528891ac7a795ebd46f7df3b7739bcdc000b3b (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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
from typing import Any, Dict
from weakref import ref

from flask import Blueprint
from kombu import Connection
from rethinkdb.ast import Table

from pysite.constants import (
    BOT_EVENT_QUEUE, BotEventTypes,
    RMQ_HOST, RMQ_PASSWORD, RMQ_PORT, RMQ_USERNAME
)
from pysite.database import RethinkDB
from pysite.oauth import OAuthBackend


BOT_EVENT_REQUIRED_PARAMS = {
    "mod_log": ("level", "title", "message"),
    "send_message": ("target", "message"),
    "send_embed": ("target",),
    "add_role": ("target", "role_id", "reason"),
    "remove_role": ("target", "role_id", "reason")
}


class DBMixin:
    """
    Mixin for classes that make use of RethinkDB. It can automatically create a table with the specified primary
    key using the attributes set at class-level.

    This class is intended to be mixed in alongside one of the other view classes. For example:

    >>> class MyView(APIView, DBMixin):
    ...     name = "my_view"  # Flask internal name for this route
    ...     path = "/my_view"  # Actual URL path to reach this route
    ...     table_name = "my_table"  # Name of the table to create
    ...     table_primary_key = "username"  # Primary key to set for this table

    This class will also work with Websockets:

    >>> class MyWebsocket(WS, DBMixin):
    ...     name = "my_websocket"
    ...     path = "/my_websocket"
    ...     table_name = "my_table"
    ...     table_primary_key = "username"

    You may omit `table_primary_key` and it will be defaulted to RethinkDB's default column - "id".
    """

    table_name = ""  # type: str
    table_primary_key = "id"  # type: str

    @classmethod
    def setup(cls: "DBMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint):
        """
        Set up the view by creating the table specified by the class attributes - this will also deal with multiple
        inheritance by calling `super().setup()` as appropriate.

        :param manager: Instance of the current RouteManager (used to get a handle for the database object)
        :param blueprint: Current Flask blueprint
        """

        if hasattr(super(), "setup"):
            super().setup(manager, blueprint)  # pragma: no cover

        cls._db = ref(manager.db)

    @property
    def table(self) -> Table:
        return self.db.query(self.table_name)

    @property
    def db(self) -> RethinkDB:
        return self._db()


class RMQMixin:
    """
    Mixin for classes that make use of RabbitMQ. It allows routes to send JSON-encoded messages to specific RabbitMQ
    queues.

    This class is intended to be mixed in alongside one of the other view classes. For example:

    >>> class MyView(APIView, RMQMixin):
    ...     name = "my_view"  # Flask internal name for this route
    ...     path = "/my_view"  # Actual URL path to reach this route
    ...     queue_name = "my_queue"  # Name of the RabbitMQ queue to send on

    Note that the queue name is optional if all you want to do is send bot events.

    This class will also work with Websockets:

    >>> class MyWebsocket(WS, RMQMixin):
    ...     name = "my_websocket"
    ...     path = "/my_websocket"
    ...     queue_name = "my_queue"
    """

    queue_name = ""

    @classmethod
    def setup(cls: "RMQMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint):
        """
        Set up the view by calling `super().setup()` as appropriate.

        :param manager: Instance of the current RouteManager (used to get a handle for the database object)
        :param blueprint: Current Flask blueprint
        """

        if hasattr(super(), "setup"):
            super().setup(manager, blueprint)  # pragma: no cover

    @property
    def rmq_connection(self) -> Connection:
        """
        Get a Kombu AMQP connection object - use this in a context manager so that it gets closed after you're done

        If you're just trying to send a message, check out `rmq_send` and `rmq_bot_event` instead.
        """

        return Connection(hostname=RMQ_HOST, userid=RMQ_USERNAME, password=RMQ_PASSWORD, port=RMQ_PORT)

    def rmq_send(self, data: Dict[str, Any], routing_key: str = None):
        """
        Send some data to the RabbitMQ queue

        >>> self.rmq_send({
        ...     "text": "My hovercraft is full of eels!",
        ...     "source": "Dirty Hungarian Phrasebook"
        ... })
        ...

        This will be delivered to the queue immediately.
        """

        if routing_key is None:
            routing_key = self.queue_name

        with self.rmq_connection as c:
            producer = c.Producer()
            producer.publish(data, routing_key=routing_key)

    def rmq_bot_event(self, event_type: BotEventTypes, data: Dict[str, Any]):
        """
        Send an event to the queue responsible for delivering events to the bot

        >>> self.rmq_bot_event(BotEventTypes.send_message, {
        ...     "channel": CHANNEL_MOD_LOG,
        ...     "message": "This is a plain-text message for @everyone, from the site!"
        ... })
        ...

        This will be delivered to the bot and actioned immediately, or when the bot comes online if it isn't already
        connected.
        """

        if not isinstance(event_type, BotEventTypes):
            raise ValueError("`event_type` must be a member of the the `pysite.constants.BotEventTypes` enum")

        event_type = event_type.value
        required_params = BOT_EVENT_REQUIRED_PARAMS[event_type]

        for param in required_params:
            if param not in data:
                raise KeyError(f"Event is missing required parameter: {param}")

        return self.rmq_send(
            {"event": event_type, "data": data},
            routing_key=BOT_EVENT_QUEUE,
        )


class OAuthMixin:
    """
    Mixin for the classes that need access to a logged in user's information. This class should be used
    to grant route's access to user information, such as name, email, id, ect.

    There will almost never be a need for someone to inherit this, as BaseView does that for you.

    This class will add 3 properties to your route:

        * logged_in (bool): True if user is registered with the site, False else wise.

        * user_data (dict): A dict that looks like this:

        {
            "user_id": Their discord ID,
            "username": Their discord username (without discriminator),
            "discriminator": Their discord discriminator,
            "email": Their email, in which is connected to discord
        }

        user_data returns None, if the user isn't logged in.

        * oauth (OAuthBackend): The instance of pysite.oauth.OAuthBackend, connected to the RouteManager.
    """

    @classmethod
    def setup(cls: "OAuthMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint):
        if hasattr(super(), "setup"):
            super().setup(manager, blueprint)  # pragma: no cover

        cls._oauth = ref(manager.oauth_backend)

    @property
    def logged_in(self) -> bool:
        return self.user_data is not None

    @property
    def user_data(self) -> dict:
        return self.oauth.user_data()

    @property
    def oauth(self) -> OAuthBackend:
        return self._oauth()