Skip to content

Commit 179fa07

Browse files
committed
Add room posting/editing endpoints
Fixes some issues around how whispers were being handled/returned, as well as a few miscellaneous small fixes in the edit code found by the added tests.
1 parent c44dc0c commit 179fa07

File tree

8 files changed

+573
-44
lines changed

8 files changed

+573
-44
lines changed

api.yaml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,14 @@ paths:
135135
- $ref: "#/components/parameters/pathRoomToken"
136136
requestBody:
137137
description: >
138-
JSON body containing the message details to be submitted to the room. Note that the
139-
`session_id` field is not contained in the response (since it is simply the client's own
140-
ID).
138+
JSON body containing the message details to be submitted to the room.
139+
140+
141+
data and signature are required; whisper_to and whisper_mods are permitted only for
142+
moderators/admins. The former sends a whisper within the room that will only be shown by
143+
the given user; the latter sends a whisper in the room that will be seen by all moderators.
144+
It is possible for a message to use both of these whisper modes at once, for example to
145+
(privately) warn a user and have that warning visible by all other moderators.
141146
required: true
142147
content:
143148
application/json:
@@ -157,6 +162,12 @@ paths:
157162
Base64-encoded message data XEd25519 signature, signed by the poster's X25519
158163
key contained in the session ID.
159164
example: NjgwYzFjOGI0YTljNTliNDk1MDRmMzY5YzFmMzRkYjM4ZTU3Mzk2YzA2ODYwODk3MzI1ZmFhMjNkYTZmNzE3YTk3MmY4MTJjZDU1MGFkMTQ2Yzk1MTdlOGM1NzMyZjgxZDE3NWViODg5OGQxZjQyMjg5ZWNkNjNjODJiMDZjNzM=
165+
whisper_to:
166+
$ref: "#/components/schemas/SessionID"
167+
whisper_mods:
168+
type: boolean
169+
description: >
170+
True if this message is a whisper that should be visible to all room moderators.
160171
files:
161172
type: array
162173
description: >
@@ -257,11 +268,11 @@ paths:
257268
$ref: "#/paths/~1room~1%7BroomToken%7D~1message/post/requestBody"
258269
responses:
259270
200:
260-
description: Message updated successfully
271+
description: Message updated successfully; returns an empty dict as body.
261272
content:
262273
application/json:
263274
schema:
264-
$ref: "#/components/schemas/Message"
275+
type: object
265276
403:
266277
description: >
267278
Forbidden. Returned if the user does not have permission to post messages to this room,

sogs/http.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# basic success:
1+
# success codes:
22
OK = 200
3+
CREATED = 201
34

45
# error status codes:
56
BAD_REQUEST = 400

sogs/model/exc.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ def __init__(self, msg=None):
6464
super().__init__("Permission denied" if msg is None else msg)
6565

6666

67+
class InvalidData(RuntimeError):
68+
"""Thrown if something in model was fed invalid data, for example a signature of an invalid
69+
size, or an unparseable entity."""
70+
71+
6772
class PostRejected(RuntimeError):
6873
"""
6974
Thrown when a post is refused for some reason other than a permission error (e.g. the post
@@ -95,3 +100,8 @@ def abort_perm_denied(e):
95100
@app.errorhandler(PostRejected)
96101
def abort_post_rejected(e):
97102
flask.abort(http.TOO_MANY_REQUESTS)
103+
104+
105+
@app.errorhandler(InvalidData)
106+
def abort_invalid_data(e):
107+
flask.abort(http.BAD_REQUEST)

sogs/model/room.py

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
BadPermission,
1313
PostRejected,
1414
PostRateLimited,
15+
InvalidData,
1516
)
1617

1718
import os
@@ -503,6 +504,24 @@ def get_messages_for(
503504
if after <= max_old_id:
504505
after += offset
505506

507+
whisper_clause = (
508+
# For a mod we want to see:
509+
# - all whisper_mods messsages
510+
# - anything directed to us specifically
511+
# - anything we sent (i.e. outbound whispers)
512+
# - non-whispers
513+
"whisper_mods OR whisper = :user OR user = :user OR whisper IS NULL"
514+
if mod
515+
# For a regular user we want to see:
516+
# - anything with whisper_to sent to us
517+
# - non-whispers
518+
else "whisper = :user OR (whisper IS NULL AND NOT whisper_mods)"
519+
if user
520+
# Otherwise for public, non-user access we want to see:
521+
# - non-whispers
522+
else "whisper IS NULL AND NOT whisper_mods"
523+
)
524+
506525
for row in query(
507526
f"""
508527
SELECT * FROM message_details
@@ -513,11 +532,7 @@ def get_messages_for(
513532
'AND id = :single' if single is not None else
514533
''
515534
}
516-
AND (
517-
whisper IS NULL
518-
{'OR whisper = :user' if user else ''}
519-
{'OR whisper_mods' if mod else ''}
520-
)
535+
AND ({whisper_clause})
521536
{
522537
'' if single is not None else
523538
'ORDER BY id ASC LIMIT :limit' if after is not None else
@@ -590,6 +605,9 @@ def add_post(
590605
if not self.check_write(user):
591606
raise BadPermission()
592607

608+
if data is None or sig is None or len(sig) != 32:
609+
raise InvalidData()
610+
593611
whisper_mods = bool(whisper_mods)
594612
if (whisper_to or whisper_mods) and not self.check_moderator(user):
595613
app.logger.warning(f"Cannot post a whisper to {self}: {user} is not a moderator")
@@ -655,6 +673,64 @@ def add_post(
655673
send_mule("message_posted", msg['id'])
656674
return msg
657675

676+
def edit_post(self, user: User, msg_id: int, data: bytes, sig: bytes):
677+
"""
678+
Edits a post in the room. The post must exist, must have been authored by the same user,
679+
and must not be deleted. The user must *currently* have write permission (i.e. if they lose
680+
write permission they cannot edit existing posts made before they were restricted).
681+
682+
Edits cannot alter the whisper_to/whisper_mods properties.
683+
684+
Raises:
685+
- BadPermission() if attempting to edit another user's message or not having write
686+
permission in the room.
687+
- A subclass of PostRejected() if the edit is unacceptable, for instance for triggering the
688+
profanity filter.
689+
- NoSuchPost() if the post is deleted.
690+
"""
691+
if not self.check_write(user):
692+
raise BadPermission()
693+
694+
if data is None or sig is None or len(sig) != 32:
695+
raise InvalidData()
696+
697+
filtered = self.should_filter(user, data)
698+
with db.transaction():
699+
author = query(
700+
'''
701+
SELECT "user" FROM messages
702+
WHERE id = :m AND room = :r AND data IS NOT NULL
703+
''',
704+
m=msg_id,
705+
r=self.id,
706+
).first()
707+
if author is None:
708+
raise NoSuchPost()
709+
author = author[0]
710+
if author != user.id:
711+
raise BadPermission()
712+
713+
if filtered:
714+
# Silent filtering is enabled and the edit failed the filter, so we want to drop the
715+
# actual post update.
716+
return
717+
718+
data_size = len(data)
719+
unpadded_data = utils.remove_session_message_padding(data)
720+
721+
query(
722+
"""
723+
UPDATE messages SET
724+
data = :data, data_size = :data_size, signature = :sig WHERE id = :m
725+
""",
726+
m=msg_id,
727+
data=unpadded_data,
728+
data_size=data_size,
729+
sig=sig,
730+
)
731+
732+
send_mule("message_edited", msg_id)
733+
658734
def delete_posts(self, message_ids: List[int], deleter: User):
659735
"""
660736
Deletes the messages with the given ids. The given user performing the delete must be a

sogs/routes/rooms.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from .. import config, http, utils
22
from ..model import room as mroom
3+
from . import auth
34

4-
from flask import abort, jsonify, g, Blueprint
5+
from flask import abort, jsonify, g, Blueprint, request
56

67
# General purpose routes for things like capability retrieval and batching
78

@@ -126,6 +127,41 @@ def message_single(room, msg_id):
126127
return utils.jsonify_with_base64(msgs[0])
127128

128129

130+
@rooms.post("/room/<Room:room>/message")
131+
@auth.user_required
132+
def post_message(room):
133+
req = request.json
134+
135+
# TODO: files tracking
136+
137+
msg = room.add_post(
138+
g.user,
139+
data=utils.decode_base64(req.get('data')),
140+
sig=utils.decode_base64(req.get('signature')),
141+
whisper_to=req.get('whisper_to'),
142+
whisper_mods=bool(req.get('whisper_mods')),
143+
)
144+
145+
return utils.jsonify_with_base64(msg), http.CREATED
146+
147+
148+
@rooms.put("/room/<Room:room>/message/<int:msg_id>")
149+
@auth.user_required
150+
def edit_message(room, msg_id):
151+
req = request.json
152+
153+
# TODO: files tracking
154+
155+
room.edit_post(
156+
g.user,
157+
msg_id,
158+
data=utils.decode_base64(req.get('data')),
159+
sig=utils.decode_base64(req.get('signature')),
160+
)
161+
162+
return jsonify({})
163+
164+
129165
@rooms.post("/room/<Room:room>/pin/<int:msg_id>")
130166
def message_pin(room, msg_id):
131167
room.pin(msg_id, g.user)

0 commit comments

Comments
 (0)