Skip to content

Commit 8306cee

Browse files
Update implementation of MSC4306: Thread Subscriptions to include automatic subscription conflict prevention as introduced in later drafts. (#18756)
Follows: #18674 Implements new drafts of MSC4306 --------- Signed-off-by: Olivier 'reivilibre <[email protected]> Co-authored-by: Eric Eastwood <[email protected]>
1 parent 076db0a commit 8306cee

File tree

14 files changed

+587
-99
lines changed

14 files changed

+587
-99
lines changed

changelog.d/18756.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update implementation of [MSC4306: Thread Subscriptions](https://github.com/matrix-org/matrix-doc/issues/4306) to include automatic subscription conflict prevention as introduced in later drafts.

synapse/api/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ class Codes(str, Enum):
140140
# Part of MSC4155
141141
INVITE_BLOCKED = "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED"
142142

143+
# Part of MSC4306: Thread Subscriptions
144+
MSC4306_CONFLICTING_UNSUBSCRIPTION = (
145+
"IO.ELEMENT.MSC4306.M_CONFLICTING_UNSUBSCRIPTION"
146+
)
147+
MSC4306_NOT_IN_THREAD = "IO.ELEMENT.MSC4306.M_NOT_IN_THREAD"
148+
143149

144150
class CodeMessageException(RuntimeError):
145151
"""An exception with integer code, a message string attributes and optional headers.

synapse/handlers/thread_subscriptions.py

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import logging
2+
from http import HTTPStatus
23
from typing import TYPE_CHECKING, Optional
34

4-
from synapse.api.errors import AuthError, NotFoundError
5-
from synapse.storage.databases.main.thread_subscriptions import ThreadSubscription
6-
from synapse.types import UserID
5+
from synapse.api.constants import RelationTypes
6+
from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
7+
from synapse.events import relation_from_event
8+
from synapse.storage.databases.main.thread_subscriptions import (
9+
AutomaticSubscriptionConflicted,
10+
ThreadSubscription,
11+
)
12+
from synapse.types import EventOrderings, UserID
713

814
if TYPE_CHECKING:
915
from synapse.server import HomeServer
@@ -55,42 +61,79 @@ async def subscribe_user_to_thread(
5561
room_id: str,
5662
thread_root_event_id: str,
5763
*,
58-
automatic: bool,
64+
automatic_event_id: Optional[str],
5965
) -> Optional[int]:
6066
"""Sets or updates a user's subscription settings for a specific thread root.
6167
6268
Args:
6369
requester_user_id: The ID of the user whose settings are being updated.
6470
thread_root_event_id: The event ID of the thread root.
65-
automatic: whether the user was subscribed by an automatic decision by
66-
their client.
71+
automatic_event_id: if the user was subscribed by an automatic decision by
72+
their client, the event ID that caused this.
6773
6874
Returns:
6975
The stream ID for this update, if the update isn't no-opped.
7076
7177
Raises:
7278
NotFoundError if the user cannot access the thread root event, or it isn't
73-
known to this homeserver.
79+
known to this homeserver. Ditto for the automatic cause event if supplied.
80+
81+
SynapseError(400, M_NOT_IN_THREAD): if client supplied an automatic cause event
82+
but user cannot access the event.
83+
84+
SynapseError(409, M_SKIPPED): if client requested an automatic subscription
85+
but it was skipped because the cause event is logically later than an unsubscription.
7486
"""
7587
# First check that the user can access the thread root event
7688
# and that it exists
7789
try:
78-
event = await self.event_handler.get_event(
90+
thread_root_event = await self.event_handler.get_event(
7991
user_id, room_id, thread_root_event_id
8092
)
81-
if event is None:
93+
if thread_root_event is None:
8294
raise NotFoundError("No such thread root")
8395
except AuthError:
8496
logger.info("rejecting thread subscriptions change (thread not accessible)")
8597
raise NotFoundError("No such thread root")
8698

87-
return await self.store.subscribe_user_to_thread(
99+
if automatic_event_id:
100+
autosub_cause_event = await self.event_handler.get_event(
101+
user_id, room_id, automatic_event_id
102+
)
103+
if autosub_cause_event is None:
104+
raise NotFoundError("Automatic subscription event not found")
105+
relation = relation_from_event(autosub_cause_event)
106+
if (
107+
relation is None
108+
or relation.rel_type != RelationTypes.THREAD
109+
or relation.parent_id != thread_root_event_id
110+
):
111+
raise SynapseError(
112+
HTTPStatus.BAD_REQUEST,
113+
"Automatic subscription must use an event in the thread",
114+
errcode=Codes.MSC4306_NOT_IN_THREAD,
115+
)
116+
117+
automatic_event_orderings = EventOrderings.from_event(autosub_cause_event)
118+
else:
119+
automatic_event_orderings = None
120+
121+
outcome = await self.store.subscribe_user_to_thread(
88122
user_id.to_string(),
89-
event.room_id,
123+
room_id,
90124
thread_root_event_id,
91-
automatic=automatic,
125+
automatic_event_orderings=automatic_event_orderings,
92126
)
93127

128+
if isinstance(outcome, AutomaticSubscriptionConflicted):
129+
raise SynapseError(
130+
HTTPStatus.CONFLICT,
131+
"Automatic subscription obsoleted by an unsubscription request.",
132+
errcode=Codes.MSC4306_CONFLICTING_UNSUBSCRIPTION,
133+
)
134+
135+
return outcome
136+
94137
async def unsubscribe_user_from_thread(
95138
self, user_id: UserID, room_id: str, thread_root_event_id: str
96139
) -> Optional[int]:

synapse/replication/tcp/streams/_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ class ThreadSubscriptionsStreamRow:
739739
NAME = "thread_subscriptions"
740740
ROW_TYPE = ThreadSubscriptionsStreamRow
741741

742-
def __init__(self, hs: Any):
742+
def __init__(self, hs: "HomeServer"):
743743
self.store = hs.get_datastores().main
744744
super().__init__(
745745
hs.get_instance_name(),
@@ -751,7 +751,7 @@ async def _update_function(
751751
self, instance_name: str, from_token: int, to_token: int, limit: int
752752
) -> StreamUpdateResult:
753753
updates = await self.store.get_updated_thread_subscriptions(
754-
from_token, to_token, limit
754+
from_id=from_token, to_id=to_token, limit=limit
755755
)
756756
rows = [
757757
(

synapse/rest/client/thread_subscriptions.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from http import HTTPStatus
2-
from typing import TYPE_CHECKING, Tuple
2+
from typing import TYPE_CHECKING, Optional, Tuple
33

4-
from synapse._pydantic_compat import StrictBool
54
from synapse.api.errors import Codes, NotFoundError, SynapseError
65
from synapse.http.server import HttpServer
76
from synapse.http.servlet import (
@@ -12,6 +11,7 @@
1211
from synapse.rest.client._base import client_patterns
1312
from synapse.types import JsonDict, RoomID
1413
from synapse.types.rest import RequestBodyModel
14+
from synapse.util.pydantic_models import AnyEventId
1515

1616
if TYPE_CHECKING:
1717
from synapse.server import HomeServer
@@ -32,7 +32,12 @@ def __init__(self, hs: "HomeServer"):
3232
self.handler = hs.get_thread_subscriptions_handler()
3333

3434
class PutBody(RequestBodyModel):
35-
automatic: StrictBool
35+
automatic: Optional[AnyEventId]
36+
"""
37+
If supplied, the event ID of an event giving rise to this automatic subscription.
38+
39+
If omitted, this subscription is a manual subscription.
40+
"""
3641

3742
async def on_GET(
3843
self, request: SynapseRequest, room_id: str, thread_root_id: str
@@ -63,15 +68,15 @@ async def on_PUT(
6368
raise SynapseError(
6469
HTTPStatus.BAD_REQUEST, "Invalid event ID", errcode=Codes.INVALID_PARAM
6570
)
66-
requester = await self.auth.get_user_by_req(request)
67-
6871
body = parse_and_validate_json_object_from_request(request, self.PutBody)
6972

73+
requester = await self.auth.get_user_by_req(request)
74+
7075
await self.handler.subscribe_user_to_thread(
7176
requester.user,
7277
room_id,
7378
thread_root_id,
74-
automatic=body.automatic,
79+
automatic_event_id=body.automatic,
7580
)
7681

7782
return HTTPStatus.OK, {}

0 commit comments

Comments
 (0)