|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from .. import crypto, db, config |
| 4 | +from ..db import query |
| 5 | +from ..web import app |
| 6 | +from .exc import BadPermission, PostRateLimited |
| 7 | +from .. import utils |
| 8 | +from ..omq import send_mule |
| 9 | +from .user import User |
| 10 | +from .room import Room |
| 11 | +from .message import Message |
| 12 | +from .filter import SimpleFilter |
| 13 | +from .exc import InvalidData |
| 14 | + |
| 15 | +from typing import Optional, List, Union |
| 16 | +import time |
| 17 | + |
| 18 | +""" |
| 19 | +Complications: |
| 20 | + - given captcha bot |
| 21 | + - new user (not approved) posts message |
| 22 | + - we need bot to reply with whisper to that user with simple problem |
| 23 | + - what does the bot do with the message they tried to send? |
| 24 | + - can store locally |
| 25 | + - user sends reply |
| 26 | + - bot inserts it into room |
| 27 | +""" |
| 28 | + |
| 29 | +""" |
| 30 | +Control Flow: |
| 31 | + 1) message comes in HTTP request |
| 32 | + 2) unpacked/parsed/verified/permissions checked |
| 33 | + 3) comes into relevant route (ex: add_post()) |
| 34 | + 4) sends off to mule to be handled by bots |
| 35 | + 5) mule has ordered list of bots by priority |
| 36 | + 6) mule passes message to bots, which have fixed return values (insert, do not insert) |
| 37 | + 7) if all bots approve, mule replies to worker with go ahead or vice versa for no go |
| 38 | +""" |
| 39 | + |
| 40 | + |
| 41 | +class BotHandler: |
| 42 | + """ |
| 43 | + Class representing an interface that manages active bots |
| 44 | +
|
| 45 | + Object Properties: |
| 46 | + bots - list of bots attached to room |
| 47 | + """ |
| 48 | + |
| 49 | + def __init__( |
| 50 | + self, |
| 51 | + _rooms: List[Room], |
| 52 | + row=None, |
| 53 | + *, |
| 54 | + id: Optional[int] = None, |
| 55 | + session_id: Optional[int] = None, |
| 56 | + ) -> None: |
| 57 | + # immutable attributes |
| 58 | + self.id = id |
| 59 | + |
| 60 | + def check_permission_for( |
| 61 | + self, |
| 62 | + room: Room, |
| 63 | + user: Optional[User] = None, |
| 64 | + *, |
| 65 | + admin=False, |
| 66 | + moderator=False, |
| 67 | + read=False, |
| 68 | + accessible=False, |
| 69 | + write=False, |
| 70 | + upload=False, |
| 71 | + ): |
| 72 | + """ |
| 73 | + Checks whether `user` has the required permissions for this room and isn't banned. Returns |
| 74 | + True if the user satisfies the permissions, False otherwise. If no user is provided then |
| 75 | + permissions are checked against the room's defaults. |
| 76 | +
|
| 77 | + Looked up permissions are cached within the Room instance so that looking up the same user |
| 78 | + multiple times (i.e. from multiple parts of the code) does not re-query the database. |
| 79 | +
|
| 80 | + Named arguments are as follows: |
| 81 | + - admin -- if true then the user must have admin access to the room |
| 82 | + - moderator -- if true then the user must have moderator (or admin) access to the room |
| 83 | + - read -- if true then the user must have read access |
| 84 | + - accessible -- if true then the user must have accessible access; note that this permission |
| 85 | + is satisfied by *either* the `accessible` or `read` database flags (that is: read implies |
| 86 | + accessible). |
| 87 | + - write -- if true then the user must have write access |
| 88 | + - upload -- if true then the user must have upload access; this should usually be combined |
| 89 | + with write=True. |
| 90 | +
|
| 91 | + You can specify multiple permissions as True, in which case all must be satisfied. If you |
| 92 | + specify no permissions as required then the check only checks whether a user is banned but |
| 93 | + otherwise requires no specific permission. |
| 94 | + """ |
| 95 | + |
| 96 | + if user is None: |
| 97 | + is_banned, can_read, can_access, can_write, can_upload, is_mod, is_admin = ( |
| 98 | + False, |
| 99 | + bool(room.default_read), |
| 100 | + bool(room.default_accessible), |
| 101 | + bool(room.default_write), |
| 102 | + bool(room.default_upload), |
| 103 | + False, |
| 104 | + False, |
| 105 | + ) |
| 106 | + else: |
| 107 | + if user.id not in self._perm_cache: |
| 108 | + row = query( |
| 109 | + """ |
| 110 | + SELECT banned, read, accessible, write, upload, moderator, admin |
| 111 | + FROM user_permissions |
| 112 | + WHERE room = :r AND "user" = :u |
| 113 | + """, |
| 114 | + r=self.id, |
| 115 | + u=user.id, |
| 116 | + ).first() |
| 117 | + self._perm_cache[user.id] = [bool(c) for c in row] |
| 118 | + |
| 119 | + ( |
| 120 | + is_banned, |
| 121 | + can_read, |
| 122 | + can_access, |
| 123 | + can_write, |
| 124 | + can_upload, |
| 125 | + is_mod, |
| 126 | + is_admin, |
| 127 | + ) = self._perm_cache[user.id] |
| 128 | + |
| 129 | + # Shortcuts for check_permission calls |
| 130 | + def check_unbanned(self, room: Room, user: Optional[User]): |
| 131 | + return self.check_permission_for(room, user) |
| 132 | + |
| 133 | + def check_read(self, room: Room, user: Optional[User] = None): |
| 134 | + return self.check_permission_for(room, user, read=True) |
| 135 | + |
| 136 | + def check_accessible(self, room: Room, user: Optional[User] = None): |
| 137 | + return self.check_permission_for(room, user, accessible=True) |
| 138 | + |
| 139 | + def check_write(self, room: Room, user: Optional[User] = None): |
| 140 | + return self.check_permission_for(room, user, write=True) |
| 141 | + |
| 142 | + def check_upload(self, room: Room, user: Optional[User] = None): |
| 143 | + """Checks for both upload *and* write permission""" |
| 144 | + return self.check_permission_for(room, user, write=True, upload=True) |
| 145 | + |
| 146 | + def check_moderator(self, room: Room, user: Optional[User]): |
| 147 | + return self.check_permission_for(room, user, moderator=True) |
| 148 | + |
| 149 | + def check_admin(self, room: Room, user: Optional[User]): |
| 150 | + return self.check_permission_for(room, user, admin=True) |
| 151 | + |
| 152 | + def receive_message( |
| 153 | + self, |
| 154 | + room: Room, |
| 155 | + user: User, |
| 156 | + data: bytes, |
| 157 | + sig: bytes, |
| 158 | + *, |
| 159 | + whisper_to: Optional[Union[User, str]] = None, |
| 160 | + whisper_mods: bool = False, |
| 161 | + files: List[int] = [], |
| 162 | + ): |
| 163 | + if not self.check_write(user): |
| 164 | + raise BadPermission() |
| 165 | + |
| 166 | + if data is None or sig is None or len(sig) != 64: |
| 167 | + raise InvalidData() |
| 168 | + |
| 169 | + whisper_mods = bool(whisper_mods) |
| 170 | + if (whisper_to or whisper_mods) and not self.check_moderator(user): |
| 171 | + app.logger.warning(f"Cannot post a whisper to {room}: {user} is not a moderator") |
| 172 | + raise BadPermission() |
| 173 | + |
| 174 | + if whisper_to and not isinstance(whisper_to, User): |
| 175 | + whisper_to = User(session_id=whisper_to, autovivify=True, touch=False) |
| 176 | + |
| 177 | + filtered = self.filter.read_message(user, data, room) |
| 178 | + |
| 179 | + with db.transaction(): |
| 180 | + if room.rate_limit_size and not self.check_admin(user): |
| 181 | + since_limit = time.time() - room.rate_limit_interval |
| 182 | + recent_count = query( |
| 183 | + """ |
| 184 | + SELECT COUNT(*) FROM messages |
| 185 | + WHERE room = :r AND "user" = :u AND posted >= :since |
| 186 | + """, |
| 187 | + r=self.id, |
| 188 | + u=user.id, |
| 189 | + since=since_limit, |
| 190 | + ).first()[0] |
| 191 | + |
| 192 | + if recent_count >= room.rate_limit_size: |
| 193 | + raise PostRateLimited() |
| 194 | + |
| 195 | + data_size = len(data) |
| 196 | + unpadded_data = utils.remove_session_message_padding(data) |
| 197 | + |
| 198 | + msg_id = db.insert_and_get_pk( |
| 199 | + """ |
| 200 | + INSERT INTO messages |
| 201 | + (room, "user", data, data_size, signature, filtered, whisper, whisper_mods) |
| 202 | + VALUES |
| 203 | + (:r, :u, :data, :data_size, :signature, :filtered, :whisper, :whisper_mods) |
| 204 | + """, |
| 205 | + "id", |
| 206 | + r=room.id, |
| 207 | + u=user.id, |
| 208 | + data=unpadded_data, |
| 209 | + data_size=data_size, |
| 210 | + signature=sig, |
| 211 | + filtered=filtered is not None, |
| 212 | + whisper=whisper_to.id if whisper_to else None, |
| 213 | + whisper_mods=whisper_mods, |
| 214 | + ) |
| 215 | + |
| 216 | + if files: |
| 217 | + # Take ownership of any uploaded files attached to the post: |
| 218 | + room._own_files(msg_id, files, user) |
| 219 | + |
| 220 | + assert msg_id is not None |
| 221 | + row = query("SELECT posted, seqno FROM messages WHERE id = :m", m=msg_id).first() |
| 222 | + msg = { |
| 223 | + 'id': msg_id, |
| 224 | + 'session_id': user.session_id, |
| 225 | + 'posted': row[0], |
| 226 | + 'seqno': row[1], |
| 227 | + 'data': data, |
| 228 | + 'signature': sig, |
| 229 | + 'reactions': {}, |
| 230 | + } |
| 231 | + if filtered is not None: |
| 232 | + msg['filtered'] = True |
| 233 | + if whisper_to or whisper_mods: |
| 234 | + msg['whisper'] = True |
| 235 | + msg['whisper_mods'] = whisper_mods |
| 236 | + if whisper_to: |
| 237 | + msg['whisper_to'] = whisper_to.session_id |
| 238 | + |
| 239 | + send_mule("message_posted", msg["id"]) |
| 240 | + return msg |
0 commit comments