Skip to content

Commit fce4c8a

Browse files
authored
Merge pull request #152 from majestrate/dm-delete-endpoints-2022-11-22
delete dm endpoints
2 parents 9f4efb4 + 8af33b2 commit fce4c8a

File tree

5 files changed

+99
-7
lines changed

5 files changed

+99
-7
lines changed

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ install_requires=
2121
oxenmq
2222
oxenc
2323
pyonionreq
24+
tomli
25+
sqlalchemy

sogs/model/message.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ def __init__(self, row=None, *, sender=None, recip=None, data=None):
4242
assert row is not None
4343
self._row = row
4444

45+
@staticmethod
46+
def delete_all(*, recip=None, sender=None):
47+
"""Delete all messages sent to a user or from a user.
48+
returns the number of rows affected.
49+
"""
50+
if sum(bool(x) for x in (sender, recip)) != 1:
51+
raise ValueError("delete_all(): exactly one of sender or recipient is required")
52+
53+
result = query(
54+
f"DELETE FROM inbox WHERE {'recipient' if recip else 'sender'} = :id",
55+
id=recip.id if recip else sender.id,
56+
)
57+
return result.rowcount
58+
4559
@staticmethod
4660
def to(user, since=None, limit=None):
4761
"""get all message for a user, returns a generator"""

sogs/routes/auth.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ def required_user_wrapper(*args, **kwargs):
121121
return required_user_wrapper
122122

123123

124+
def require_blind_user():
125+
"""Requires that the authenticated user is using a blinded pubkey for auth; aborts with 401
126+
Unauthorized if the user has not authenticated with a blinded pubkey."""
127+
require_user()
128+
if not g.user.is_blinded:
129+
abort_with_reason(http.UNAUTHORIZED, "This endpoint requires blinded pubkeys be used")
130+
131+
132+
def blind_user_required(f):
133+
"""Decorator for an endpoint that requires a user that is using a blinded public key;
134+
this calls `require_blind_user()` at the beginning
135+
of the request to abort the request as Unauthorized if that precondition is not met."""
136+
137+
@wraps(f)
138+
def blind_user_wrapper(*args, **kwargs):
139+
require_blind_user()
140+
return f(*args, **kwargs)
141+
142+
return blind_user_wrapper
143+
144+
124145
def require_mod(room, *, admin=False):
125146
"""Checks a room for moderator or admin permission; aborts with 401 Unauthorized if there is no
126147
user in the request, and 403 Forbidden if g.user does not have moderator (or admin, if

sogs/routes/dm.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ def _serialize_message(msg, include_message=True):
2525

2626
def _box(out: bool, *, since=None):
2727
"""handle inbox/outbox endpoints common logic"""
28-
if not g.user.is_blinded:
29-
abort(http.FORBIDDEN)
3028
limit = utils.get_int_param('limit', 100, min=1, max=256, truncate=True)
3129
get = Message.sent if out else Message.to
3230
msgs = [_serialize_message(msg) for msg in get(user=g.user, limit=limit, since=since)]
@@ -36,7 +34,7 @@ def _box(out: bool, *, since=None):
3634

3735

3836
@dm.get("/outbox")
39-
@auth.user_required
37+
@auth.blind_user_required
4038
def get_outbox():
4139
"""
4240
Retrieves all of the user's sent messages (up to `limit`).
@@ -45,7 +43,7 @@ def get_outbox():
4543

4644

4745
@dm.get("/outbox/since/<int:msgid>")
48-
@auth.user_required
46+
@auth.blind_user_required
4947
def poll_outbox(msgid):
5048
"""
5149
Polls for any DMs sent since the given id.
@@ -54,7 +52,7 @@ def poll_outbox(msgid):
5452

5553

5654
@dm.get("/inbox")
57-
@auth.user_required
55+
@auth.blind_user_required
5856
def get_inbox():
5957
"""
6058
Retrieves all of the user's recieved messages (up to `limit`).
@@ -63,7 +61,7 @@ def get_inbox():
6361

6462

6563
@dm.get("/inbox/since/<int:msgid>")
66-
@auth.user_required
64+
@auth.blind_user_required
6765
def poll_inbox(msgid):
6866
"""
6967
Polls for any DMs received since the given id.
@@ -112,3 +110,20 @@ def send_inbox(sid):
112110
with db.transaction():
113111
msg = Message(data=utils.decode_base64(message), recip=recip_user, sender=g.user)
114112
return jsonify(_serialize_message(msg, include_message=False)), http.CREATED
113+
114+
115+
@dm.delete("/inbox")
116+
@auth.blind_user_required
117+
def delete_inbox_items():
118+
"""
119+
Deletes all of the user's received messages.
120+
121+
# Return value
122+
123+
Returns a JSON object with one key `"deleted"` set to the number of deleted messages.
124+
"""
125+
ret = dict()
126+
with db.transaction():
127+
ret['deleted'] = Message.delete_all(recip=g.user)
128+
129+
return jsonify(ret), http.OK

tests/test_dm.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
from request import sogs_get, sogs_post
1+
from request import sogs_get, sogs_post, sogs_delete
22
from sogs import config
33
from sogs.hashing import blake2b
44
from sogs.utils import encode_base64
55
from sogs.model.user import SystemUser
66
import nacl.bindings as sodium
77
from nacl.utils import random
88
from util import from_now
9+
from itertools import product
910

1011

1112
def test_dm_default_empty(client, blind_user):
@@ -89,3 +90,42 @@ def test_dm_send(client, blind_user, blind_user2):
8990
assert data.pop('posted_at') == from_now.seconds(0)
9091
assert data.pop('expires_at') == from_now.seconds(config.DM_EXPIRY)
9192
assert data == msg_expected
93+
94+
95+
def test_dm_delete(client, blind_user, blind_user2):
96+
num_posts = 10
97+
for sender, recip in product((blind_user, blind_user2), repeat=2):
98+
# make DMs
99+
for n in range(num_posts):
100+
post = make_post(f"bep-{n}".encode('ascii'), sender=sender, to=recip)
101+
r = sogs_post(client, f'/inbox/{recip.session_id}', post, sender)
102+
assert r.status_code == 201
103+
104+
# get DMs
105+
r = sogs_get(client, "/inbox", recip)
106+
assert r.status_code == 200
107+
posts = r.json
108+
assert isinstance(posts, list)
109+
assert len(posts) == num_posts
110+
111+
# delete DMs
112+
r = sogs_delete(client, "/inbox", recip)
113+
assert r.status_code == 200
114+
assert r.json == {'deleted': num_posts}
115+
116+
# make sure it is empty
117+
r = sogs_get(client, "/inbox", recip)
118+
assert r.status_code == 200
119+
posts = r.json
120+
assert posts == []
121+
122+
# delete again when nothing is there
123+
r = sogs_delete(client, "/inbox", recip)
124+
assert r.status_code == 200
125+
assert r.json == {'deleted': 0}
126+
127+
# make sure it is still empty (probably redundant but good to have)
128+
r = sogs_get(client, "/inbox", recip)
129+
assert r.status_code == 200
130+
posts = r.json
131+
assert posts == []

0 commit comments

Comments
 (0)