Skip to content

Commit 2cd38c7

Browse files
committed
siloed mule and omq into classes
1 parent b28e560 commit 2cd38c7

File tree

6 files changed

+373
-111
lines changed

6 files changed

+373
-111
lines changed

contrib/uwsgi-sogs-standalone.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ processes = 2
2727
enable-threads = true
2828
http = :80
2929
mount = /=sogs.web:app
30-
mule = sogs.mule:run
30+
mule = @(call://sogs.mule.Mule.Mule)
3131
log-4xx = true
3232
log-5xx = true
3333
disable-logging = true

sogs/model/bothandler.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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

sogs/model/room.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,13 @@ def __init__(self, row=None, *, id=None, token=None, bot: Bot = None):
7979
looking up this raises a NoSuchRoom if no room with that token/id exists.
8080
"""
8181
self._refresh(id=id, token=token, row=row)
82-
self.active_bot: bool = False
83-
self.default_bot: Bot = bot if bot else Bot(_rooms=[self])
82+
self._active_bot: bool = False
8483
self.rate_limit_size = 5
8584
self.rate_limit_interval = 16.0
8685

87-
def activate_moderation(self):
88-
self.default_bot.status[self] = True
89-
self.active_bot = True
90-
91-
def filter_only(self):
92-
self.default_bot.status[self] = False
93-
self.active_bot = False
86+
@property.getter
87+
def _bot_status(self) -> bool:
88+
return self._active_bot
9489

9590
def _refresh(self, *, id=None, token=None, row=None, perms=False):
9691
"""

0 commit comments

Comments
 (0)