Skip to content

Commit 4891025

Browse files
committed
Add PUT endpoint to modify room details
1 parent 0aa366a commit 4891025

File tree

4 files changed

+324
-16
lines changed

4 files changed

+324
-16
lines changed

api.yaml

Lines changed: 61 additions & 0 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]

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/rooms.py

Lines changed: 61 additions & 1 deletion
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
@@ -58,6 +59,65 @@ def get_rooms():
5859
return jsonify([get_one_room(r) for r in mroom.get_readable_rooms(g.user)])
5960

6061

62+
BAD_NAME_CHARS = {c: None for c in range(32)}
63+
BAD_DESCRIPTION_CHARS = {c: None for c in range(32) if not (0x09 <= c <= 0x0A)}
64+
65+
66+
@rooms.put("/room/<Room:room>")
67+
@auth.user_required
68+
def update_room(room):
69+
70+
if not room.check_admin(g.user):
71+
abort(http.FORBIDDEN)
72+
73+
req = request.json
74+
75+
with db.transaction():
76+
did = False
77+
if 'name' in req:
78+
n = req['name']
79+
if not isinstance(n, str):
80+
app.logger.warning(f"Room update with invalid name: {type(n)} != str")
81+
abort(http.BAD_REQUEST)
82+
room.name = n.translate(BAD_NAME_CHARS)
83+
did = True
84+
if 'description' in req:
85+
d = req['description']
86+
if not (d is None or isinstance(d, str)):
87+
app.logger.warning(f"Room update: invalid description: {type(d)} is not str, null")
88+
abort(http.BAD_REQUEST)
89+
if d is not None:
90+
d = d.translate(BAD_DESCRIPTION_CHARS)
91+
if len(d) == 0:
92+
d = None
93+
94+
room.description = d
95+
did = True
96+
read, write, upload = (req.get('default_' + x) for x in ('read', 'write', 'upload'))
97+
for val in (read, write, upload):
98+
if not (val is None or isinstance(val, bool) or isinstance(val, int)):
99+
app.logger.warning(
100+
f"Room update: default_read/write/upload must be bool, not {type(val)}"
101+
)
102+
abort(http.BAD_REQUEST)
103+
104+
if read is not None:
105+
room.default_read = bool(read)
106+
did = True
107+
if write is not None:
108+
room.default_write = bool(write)
109+
did = True
110+
if upload is not None:
111+
room.default_upload = bool(upload)
112+
did = True
113+
114+
if not did:
115+
app.logger.warning("Room update: must include at least one field to update")
116+
abort(http.BAD_REQUEST)
117+
118+
return jsonify({})
119+
120+
61121
@rooms.get("/room/<Room:room>/pollInfo/<int:info_updated>")
62122
def poll_room_info(room, info_updated):
63123
if g.user:

tests/test_room_routes.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,188 @@ def test_list(client, room, user, user2, admin, mod, global_mod, global_admin):
134134
assert r.json == {**r3_expected, **exp_gadmin}
135135

136136

137+
def test_updates(client, room, user, user2, mod, admin, global_mod, global_admin):
138+
url_room = '/room/test-room'
139+
r = sogs_get(client, url_room, user)
140+
assert r.status_code == 200
141+
expect_room = {
142+
"token": "test-room",
143+
"name": "Test room",
144+
"description": "Test suite testing room",
145+
"info_updates": 2,
146+
"message_sequence": 0,
147+
"created": room.created,
148+
"active_users": 0,
149+
"active_users_cutoff": int(86400 * sogs.config.ROOM_DEFAULT_ACTIVE_THRESHOLD),
150+
"moderators": [mod.session_id],
151+
"admins": [admin.session_id],
152+
"read": True,
153+
"write": True,
154+
"upload": True,
155+
}
156+
assert r.json == expect_room
157+
158+
for u in (user, user2, mod, global_mod):
159+
r = sogs_put(client, url_room, {"name": "OMG ROOM!"}, u)
160+
assert r.status_code == 403
161+
162+
assert sogs_get(client, url_room, user).json == expect_room
163+
164+
r = sogs_put(client, url_room, {"name": "OMG ROOM!"}, admin)
165+
assert r.status_code == 200
166+
expect_room['name'] = "OMG ROOM!"
167+
expect_room['info_updates'] += 1
168+
169+
assert sogs_get(client, url_room, user).json == expect_room
170+
171+
r = sogs_put(
172+
client, url_room, {"name": "rrr", "description": "Tharr be pirrrates!"}, global_admin
173+
)
174+
assert r.status_code == 200
175+
expect_room['name'] = 'rrr'
176+
expect_room['description'] = 'Tharr be pirrrates!'
177+
expect_room['info_updates'] += 2
178+
179+
assert sogs_get(client, url_room, user).json == expect_room
180+
181+
r = sogs_put(client, url_room, {"default_write": False}, admin)
182+
assert r.status_code == 200
183+
expect_room['write'] = False
184+
expect_room['upload'] = False # upload requires default_upload *and* write
185+
# expect_room['info_updates'] += 0 # permission updates don't increment info_updates
186+
187+
assert sogs_get(client, url_room, user).json == expect_room
188+
189+
expect_mod = {
190+
'read': True,
191+
'write': True,
192+
'upload': True,
193+
'default_read': True,
194+
'default_write': False,
195+
'default_upload': True,
196+
'moderator': True,
197+
'moderators': [mod.session_id],
198+
'admins': [admin.session_id],
199+
'hidden_moderators': [global_mod.session_id],
200+
'hidden_admins': [global_admin.session_id],
201+
}
202+
assert sogs_get(client, url_room, mod).json == {**expect_room, **expect_mod}
203+
204+
r = sogs_put(client, url_room, {"default_upload": False, "default_read": True}, admin)
205+
assert r.status_code == 200
206+
expect_mod['default_upload'] = False
207+
208+
assert sogs_get(client, url_room, user).json == expect_room
209+
assert sogs_get(client, url_room, mod).json == {**expect_room, **expect_mod}
210+
211+
r = sogs_put(client, url_room, {"default_read": False}, admin)
212+
assert r.status_code == 200
213+
expect_room['read'] = False
214+
expect_mod['default_read'] = False
215+
216+
assert sogs_get(client, url_room, user).json == expect_room
217+
assert sogs_get(client, url_room, mod).json == {**expect_room, **expect_mod}
218+
219+
r = sogs_put(
220+
client,
221+
url_room,
222+
{
223+
"default_read": True,
224+
"default_write": True,
225+
"default_upload": True,
226+
"name": "Gudaye, mytes!",
227+
"description": (
228+
"Room for learning to speak Australian\n\n"
229+
"Throw a shrimpie on the barbie and crack a coldie from the bottle-o!"
230+
),
231+
},
232+
admin,
233+
)
234+
assert r.status_code == 200
235+
for x in ('read', 'write', 'upload'):
236+
expect_room[x] = True
237+
expect_room['name'] = "Gudaye, mytes!"
238+
expect_room['description'] = (
239+
"Room for learning to speak Australian\n\n"
240+
"Throw a shrimpie on the barbie and crack a coldie from the bottle-o!"
241+
)
242+
expect_room['info_updates'] += 2
243+
for x in ('read', 'write', 'upload'):
244+
expect_mod['default_' + x] = True
245+
246+
assert sogs_get(client, url_room, user).json == expect_room
247+
assert sogs_get(client, url_room, mod).json == {**expect_room, **expect_mod}
248+
249+
r = sogs_put(client, url_room, {"description": None}, admin)
250+
assert r.status_code == 200
251+
252+
del expect_room['description']
253+
expect_room['info_updates'] += 1
254+
255+
assert sogs_get(client, url_room, user).json == expect_room
256+
assert sogs_get(client, url_room, mod).json == {**expect_room, **expect_mod}
257+
258+
r = sogs_put(client, url_room, {"description": "ddd"}, admin)
259+
expect_room['description'] = 'ddd'
260+
expect_room['info_updates'] += 1
261+
assert sogs_get(client, url_room, user).json == expect_room
262+
263+
# empty string description should be treated as null
264+
r = sogs_put(client, url_room, {"description": ""}, admin)
265+
del expect_room['description']
266+
expect_room['info_updates'] += 1
267+
assert sogs_get(client, url_room, user).json == expect_room
268+
269+
# Name strips out all control chars (i.e. anything below \x20); description strips out all
270+
# except newline (\x0a) and tab (\x09).
271+
r = sogs_put(
272+
client,
273+
url_room,
274+
{
275+
"description": f"a{''.join(chr(c) for c in range(33))}z",
276+
"name": f"a{''.join(chr(c) for c in range(33))}z",
277+
},
278+
admin,
279+
)
280+
expect_room['description'] = 'a\x09\x0a z'
281+
expect_room['name'] = 'a z'
282+
expect_room['info_updates'] += 2
283+
284+
assert sogs_get(client, url_room, user).json == expect_room
285+
assert sogs_get(client, url_room, mod).json == {**expect_room, **expect_mod}
286+
287+
# Test bad arguments properly err:
288+
assert [
289+
sogs_put(client, url_room, data, admin).status_code
290+
for data in (
291+
{},
292+
{'name': 42},
293+
{'name': None},
294+
{'description': 42},
295+
{'default_read': "foo"},
296+
{'default_write': "bar"},
297+
{'default_upload': None},
298+
)
299+
] == [400] * 7
300+
301+
assert sogs_get(client, url_room, user).json == expect_room
302+
assert sogs_get(client, url_room, mod).json == {**expect_room, **expect_mod}
303+
304+
# Last but not least let's fill up name and description with emoji!
305+
emoname = "💰 🐈 🍌 🌋 ‽"
306+
emodesc = (
307+
"💾 🚌 🗑 📱 🆗 😴 👖 💲 🍹 📉 🍩 🛎 🚣 ⚫️ 🐕 🕒 🏕 🎩 🆕 🍭 💋 🐌 📡 🚫 "
308+
"🕢 🚮 🎳 🚠 📦 😛 ♋️ 🌼 🏭 👼 🙆 👗 🏡 😞 🎠 ⭕️ 💚 💁 💸 🌟 ☀️ 🍀 🎶 🍿"
309+
)
310+
r = sogs_put(client, url_room, {"description": emodesc, "name": emoname}, admin)
311+
expect_room['description'] = emodesc
312+
expect_room['name'] = emoname
313+
expect_room['info_updates'] += 2
314+
315+
assert sogs_get(client, url_room, user).json == expect_room
316+
assert sogs_get(client, url_room, mod).json == {**expect_room, **expect_mod}
317+
318+
137319
def test_polling(client, room, user, user2, mod, admin, global_mod, global_admin):
138320
r = sogs_get(client, "/room/test-room", user)
139321
assert r.status_code == 200

0 commit comments

Comments
 (0)