Skip to content

Commit c3cffb1

Browse files
authored
Merge pull request #48 from jagerman/room-updating
Room info update endpoint
2 parents 0aa366a + 00f812c commit c3cffb1

File tree

5 files changed

+420
-52
lines changed

5 files changed

+420
-52
lines changed

api.yaml

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,67 @@ paths:
6666
Forbidden. Returned if the user is banned from the room or otherwise does not have read
6767
access to the room.
6868
content: {}
69+
put:
70+
tags: [Rooms]
71+
summary: Updates room details/settings.
72+
parameters:
73+
- $ref: "#/components/parameters/pathRoomToken"
74+
requestBody:
75+
description:
76+
JSON body containing the room details to update. Any field can be omitted to leave it at
77+
its current value. The invoking user must have admin permissions in the room to call this
78+
method.
79+
required: true
80+
content:
81+
application/json:
82+
schema:
83+
type: object
84+
properties:
85+
name:
86+
type: string
87+
description: >
88+
New user-displayed single-line name/title of this room. UTF-8 encoded;
89+
newlines, tabs and other control characters (i.e. all codepoints below \u0020)
90+
will be stripped out.
91+
maxLength: 100
92+
example: "My New Room"
93+
description:
94+
type: string
95+
description: >
96+
Long description to show to users, typically in smaller text below the room
97+
name. UTF-8 encoded, and permits newlines, tabs; other control characters below
98+
\u0020 will be stripped out. Can be `null` or an empty string to remove the
99+
description entirely.
100+
example: "This is the room for all things new.\n\nHi mom!"
101+
default_read:
102+
type: boolean
103+
description: >
104+
Sets the default "read" permission (if true: users can read messages) for users
105+
in this room who do not otherwise have specific permissions applied.
106+
example: true
107+
default_write:
108+
type: boolean
109+
description: >
110+
Sets the default "write" permission (if true: users can post messages) for users
111+
in the room who do not otherwise have specific permissions applied.
112+
example: false
113+
default_upload:
114+
type: boolean
115+
description: >
116+
Sets the default "upload" permission (if true: users can post messages
117+
containing attachments) for users in the room who do not otherwise have
118+
specific permissions applied.
119+
example: false
120+
responses:
121+
200:
122+
description: The room was successfully updated. Currently returns an empty json dict.
123+
content:
124+
application/json:
125+
schema:
126+
type: object
127+
403:
128+
description: Forbidden. Returned if the user does not have admin permission in this room.
129+
69130
/room/{roomToken}/pollInfo/{info_updated}:
70131
get:
71132
tags: [Rooms]
@@ -123,6 +184,12 @@ paths:
123184
$ref: "#/components/schemas/Room/properties/global_moderator"
124185
global_admin:
125186
$ref: "#/components/schemas/Room/properties/global_admin"
187+
default_read:
188+
$ref: "#/components/schemas/Room/properties/default_read"
189+
default_write:
190+
$ref: "#/components/schemas/Room/properties/default_write"
191+
default_upload:
192+
$ref: "#/components/schemas/Room/properties/default_upload"
126193
details:
127194
allOf:
128195
- $ref: "#/components/schemas/Room"
@@ -1465,6 +1532,26 @@ components:
14651532
Whether the requesting user has permissions to upload attachments to messages posted to
14661533
the room. (Note that changes to this property do not cause an `info_update` increment.)
14671534
example: true
1535+
default_read:
1536+
type: boolean
1537+
description: >
1538+
Whether new users have permission to read posts in this room by default. This property
1539+
is only returned if the calling user has moderator/admin permissions.
1540+
example: true
1541+
default_write:
1542+
type: boolean
1543+
description: >
1544+
Whether new users have permission to write posts in this room by default. This property
1545+
is only returned if the calling user has moderator/admin permissions.
1546+
example: true
1547+
default_upload:
1548+
type: boolean
1549+
description: >
1550+
Whether new users have permission to upload attachments to posts in this room by
1551+
default. This property is only returned if the calling user has moderator/admin
1552+
permissions.
1553+
example: true
1554+
14681555
Message:
14691556
title: The content of a posted message
14701557
type: object
@@ -1576,9 +1663,6 @@ components:
15761663
description: "Session ID of a user."
15771664
schema:
15781665
$ref: "#/components/schemas/SessionID"
1579-
1580-
1581-
15821666

15831667
securitySchemes:
15841668
pubkey:

sogs/model/room.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,10 @@ def name(self):
213213
@name.setter
214214
def name(self, name: str):
215215
"""Sets the room's human-readable name."""
216-
with db.transaction():
217-
query("UPDATE rooms SET name = :n WHERE id = :r", r=self.id, n=name)
218-
self._refresh()
216+
if name != self._name:
217+
with db.transaction():
218+
query("UPDATE rooms SET name = :n WHERE id = :r", r=self.id, n=name)
219+
self._refresh()
219220

220221
@property
221222
def description(self):
@@ -225,9 +226,10 @@ def description(self):
225226
@description.setter
226227
def description(self, desc):
227228
"""Sets the room's human-readable description."""
228-
with db.transaction():
229-
query("UPDATE rooms SET description = :d WHERE id = :r", r=self.id, d=desc)
230-
self._refresh()
229+
if desc != self._description:
230+
with db.transaction():
231+
query("UPDATE rooms SET description = :d WHERE id = :r", r=self.id, d=desc)
232+
self._refresh()
231233

232234
@property
233235
def image_id(self):
@@ -322,25 +324,28 @@ def default_upload(self):
322324
def default_read(self, read: bool):
323325
"""Sets the default read permission of the room"""
324326

325-
with db.transaction():
326-
query("UPDATE rooms SET read = :read WHERE id = :r", r=self.id, read=read)
327-
self._refresh(perms=True)
327+
if read != self._default_read:
328+
with db.transaction():
329+
query("UPDATE rooms SET read = :read WHERE id = :r", r=self.id, read=read)
330+
self._refresh(perms=True)
328331

329332
@default_write.setter
330333
def default_write(self, write: bool):
331334
"""Sets the default write permission of the room"""
332335

333-
with db.transaction():
334-
query("UPDATE rooms SET write = :write WHERE id = :r", r=self.id, write=write)
335-
self._refresh(perms=True)
336+
if write != self._default_write:
337+
with db.transaction():
338+
query("UPDATE rooms SET write = :write WHERE id = :r", r=self.id, write=write)
339+
self._refresh(perms=True)
336340

337341
@default_upload.setter
338342
def default_upload(self, upload: bool):
339343
"""Sets the default upload permission of the room"""
340344

341-
with db.transaction():
342-
query("UPDATE rooms SET upload = :upload WHERE id = :r", r=self.id, upload=upload)
343-
self._refresh(perms=True)
345+
if upload != self._default_upload:
346+
with db.transaction():
347+
query("UPDATE rooms SET upload = :upload WHERE id = :r", r=self.id, upload=upload)
348+
self._refresh(perms=True)
344349

345350
def active_users(self, cutoff=config.ROOM_DEFAULT_ACTIVE_THRESHOLD * 86400):
346351
"""

sogs/routes/auth.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,44 @@ def required_user_wrapper(*args, **kwargs):
123123
return required_user_wrapper
124124

125125

126+
def require_mod(room, *, admin=False):
127+
"""Checks a room for moderator or admin permission; aborts with 401 Unauthorized if there is no
128+
user in the request, and 403 Forbidden if g.user does not have moderator (or admin, if
129+
specified) permission."""
130+
require_user()
131+
if not (room.check_admin(g.user) if admin else room.check_moderator(g.user)):
132+
abort_with_reason(
133+
http.FORBIDDEN,
134+
f"This endpoint requires {'admin' if admin else 'moderator'} room permissions",
135+
)
136+
137+
138+
def mod_required(f):
139+
"""Decorator for an endpoint that requires a user that has moderator permission in the given
140+
room. The function must take a `room` argument by name, as is typically used with flask
141+
endpoints with a <Room:room> argument."""
142+
143+
@wraps(f)
144+
def required_mod_wrapper(*args, room, **kwargs):
145+
require_mod(room)
146+
return f(*args, room=room, **kwargs)
147+
148+
return required_mod_wrapper
149+
150+
151+
def admin_required(f):
152+
"""Decorator for an endpoint that requires a user that has admin permission in the given room.
153+
The function must take a `room` argument by name, as is typically used with flask endpoints with
154+
a <Room:room> argument."""
155+
156+
@wraps(f)
157+
def required_admin_wrapper(*args, room, **kwargs):
158+
require_mod(room, admin=True)
159+
return f(*args, room=room, **kwargs)
160+
161+
return required_admin_wrapper
162+
163+
126164
@app.before_request
127165
def handle_http_auth():
128166
"""

sogs/routes/rooms.py

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from .. import config, http, utils
1+
from .. import config, db, http, utils
22
from ..model import room as mroom
3+
from ..web import app
34
from . import auth
45

56
from flask import abort, jsonify, g, Blueprint, request
@@ -17,21 +18,21 @@ def get_one_room(room):
1718
rr = {
1819
'token': room.token,
1920
'name': room.name,
20-
'description': room.description,
2121
'info_updates': room.info_updates,
2222
'message_sequence': room.message_sequence,
2323
'created': room.created,
2424
'active_users': room.active_users(),
2525
'active_users_cutoff': int(config.ROOM_DEFAULT_ACTIVE_THRESHOLD * 86400),
2626
'moderators': mods,
2727
'admins': admins,
28-
'moderator': room.check_moderator(g.user),
29-
'admin': room.check_admin(g.user),
3028
'read': room.check_read(g.user),
3129
'write': room.check_write(g.user),
3230
'upload': room.check_upload(g.user),
3331
}
3432

33+
if room.description is not None:
34+
rr['description'] = room.description
35+
3536
if room.image_id is not None:
3637
rr['image_id'] = room.image_id
3738

@@ -44,6 +45,13 @@ def get_one_room(room):
4445
if h_admins:
4546
rr['hidden_admins'] = h_admins
4647

48+
if room.check_moderator(g.user):
49+
rr['moderator'] = True
50+
rr['default_read'] = room.default_read
51+
rr['default_write'] = room.default_write
52+
rr['default_upload'] = room.default_upload
53+
if room.check_admin(g.user):
54+
rr['admin'] = True
4755
if g.user:
4856
if g.user.global_moderator:
4957
rr['global_moderator'] = True
@@ -58,6 +66,62 @@ def get_rooms():
5866
return jsonify([get_one_room(r) for r in mroom.get_readable_rooms(g.user)])
5967

6068

69+
BAD_NAME_CHARS = {c: None for c in range(32)}
70+
BAD_DESCRIPTION_CHARS = {c: None for c in range(32) if not (0x09 <= c <= 0x0A)}
71+
72+
73+
@rooms.put("/room/<Room:room>")
74+
@auth.admin_required
75+
def update_room(room):
76+
77+
req = request.json
78+
79+
with db.transaction():
80+
did = False
81+
if 'name' in req:
82+
n = req['name']
83+
if not isinstance(n, str):
84+
app.logger.warning(f"Room update with invalid name: {type(n)} != str")
85+
abort(http.BAD_REQUEST)
86+
room.name = n.translate(BAD_NAME_CHARS)
87+
did = True
88+
if 'description' in req:
89+
d = req['description']
90+
if not (d is None or isinstance(d, str)):
91+
app.logger.warning(f"Room update: invalid description: {type(d)} is not str, null")
92+
abort(http.BAD_REQUEST)
93+
if d is not None:
94+
d = d.translate(BAD_DESCRIPTION_CHARS)
95+
if len(d) == 0:
96+
d = None
97+
98+
room.description = d
99+
did = True
100+
read, write, upload = (req.get('default_' + x) for x in ('read', 'write', 'upload'))
101+
for val in (read, write, upload):
102+
if not (val is None or isinstance(val, bool) or isinstance(val, int)):
103+
app.logger.warning(
104+
f"Room update: default_read/write/upload must be bool, not {type(val)}"
105+
)
106+
abort(http.BAD_REQUEST)
107+
108+
if read is not None:
109+
room.default_read = bool(read)
110+
did = True
111+
if write is not None:
112+
room.default_write = bool(write)
113+
did = True
114+
if upload is not None:
115+
room.default_upload = bool(upload)
116+
did = True
117+
118+
if not did:
119+
app.logger.warning("Room update: must include at least one field to update")
120+
abort(http.BAD_REQUEST)
121+
122+
return jsonify({})
123+
124+
61125
@rooms.get("/room/<Room:room>/pollInfo/<int:info_updated>")
62126
def poll_room_info(room, info_updated):
63127
if g.user:
@@ -66,8 +130,6 @@ def poll_room_info(room, info_updated):
66130
result = {
67131
'token': room.token,
68132
'active_users': room.active_users(),
69-
'moderator': room.check_moderator(g.user),
70-
'admin': room.check_admin(g.user),
71133
'read': room.check_read(g.user),
72134
'write': room.check_write(g.user),
73135
'upload': room.check_upload(g.user),
@@ -76,6 +138,13 @@ def poll_room_info(room, info_updated):
76138
if room.info_updates != info_updated:
77139
result['details'] = get_one_room(room)
78140

141+
if room.check_moderator(g.user):
142+
result['moderator'] = True
143+
result['default_read'] = room.default_read
144+
result['default_write'] = room.default_write
145+
result['default_upload'] = room.default_upload
146+
if room.check_admin(g.user):
147+
result['admin'] = True
79148
if g.user:
80149
if g.user.global_moderator:
81150
result['global_moderator'] = True

0 commit comments

Comments
 (0)