Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Support for MSC2285 (hidden read receipts) #10413

Merged
merged 26 commits into from
Jul 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
31d023a
Add MSC2285
SimonBrandner Jul 14, 2021
2b28a13
Implement MSC2285
SimonBrandner Jul 15, 2021
c7ff003
Test that hidden read receipts don't break unread counts
SimonBrandner Jul 15, 2021
329ca23
Test that hidden read receipts are hidden
SimonBrandner Jul 15, 2021
5ba7be7
Test filtering of hidden events
SimonBrandner Jul 16, 2021
d11783c
Changelog
SimonBrandner Jul 16, 2021
c352751
Make filter_out_hidden static
SimonBrandner Jul 19, 2021
1dcccd3
Handling of hidden read receipts for initial sync
SimonBrandner Jul 19, 2021
59763c4
Correctly handle hidden=True
SimonBrandner Jul 19, 2021
39f830c
_filters_correctly -> _test_filters_hidden
SimonBrandner Jul 26, 2021
b8ece44
Add an MSC2285_HIDDEN const
SimonBrandner Jul 26, 2021
b90949b
Use correct param name
SimonBrandner Jul 26, 2021
93c87aa
Use 400 for consistency
SimonBrandner Jul 26, 2021
77cd0d8
Add a comment about not using prefixes
SimonBrandner Jul 26, 2021
15eca46
Split out into seperate methods
SimonBrandner Jul 26, 2021
dc4e8ac
Default to hidden = None
SimonBrandner Jul 26, 2021
397ffea
Iterate over both keys and values
SimonBrandner Jul 26, 2021
fa90c66
test_read_receipts -> test_hidden_read_receipts
SimonBrandner Jul 26, 2021
d0d4dc6
Hide MSC2285 behind a feature flag
SimonBrandner Jul 26, 2021
7743ab8
Delint
SimonBrandner Jul 26, 2021
8fd2929
Merge remote-tracking branch 'upstream/develop' into feature/hidden-rrs
SimonBrandner Jul 26, 2021
49dfd1d
Move comment
SimonBrandner Jul 27, 2021
944ee17
Make if statement more readable
SimonBrandner Jul 27, 2021
a02cf57
Move comment to an even better place
SimonBrandner Jul 27, 2021
ee87ab5
Merge branch 'feature/hidden-rrs' of https://github.com/SimonBrandner…
SimonBrandner Jul 27, 2021
f6ba3ac
Update changelog.d/10413.feature
babolivier Jul 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/10413.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-doc/pull/2285). Contributed by @SimonBrandner.
4 changes: 4 additions & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,7 @@ class HistoryVisibility:
JOINED = "joined"
SHARED = "shared"
WORLD_READABLE = "world_readable"


class ReadReceiptEventFields:
MSC2285_HIDDEN = "org.matrix.msc2285.hidden"
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ def read_config(self, config: JsonDict, **kwargs):
# MSC2716 (backfill existing history)
self.msc2716_enabled: bool = experimental.get("msc2716_enabled", False)

# MSC2285 (hidden read receipts)
self.msc2285_enabled: bool = experimental.get("msc2285_enabled", False)

# MSC3244 (room version capabilities)
self.msc3244_enabled: bool = experimental.get("msc3244_enabled", False)
7 changes: 6 additions & 1 deletion synapse/handlers/initial_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from synapse.api.errors import SynapseError
from synapse.events.validator import EventValidator
from synapse.handlers.presence import format_user_presence_state
from synapse.handlers.receipts import ReceiptEventSource
from synapse.logging.context import make_deferred_yieldable, run_in_background
from synapse.storage.roommember import RoomsForUser
from synapse.streams.config import PaginationConfig
Expand Down Expand Up @@ -134,6 +135,8 @@ async def _snapshot_all_rooms(
joined_rooms,
to_key=int(now_token.receipt_key),
)
if self.hs.config.experimental.msc2285_enabled:
receipt = ReceiptEventSource.filter_out_hidden(receipt, user_id)

tags_by_room = await self.store.get_tags_for_user(user_id)

Expand Down Expand Up @@ -430,7 +433,9 @@ async def get_receipts():
room_id, to_key=now_token.receipt_key
)
if not receipts:
receipts = []
return []
if self.hs.config.experimental.msc2285_enabled:
receipts = ReceiptEventSource.filter_out_hidden(receipts, user_id)
return receipts

presence, receipts, (messages, token) = await make_deferred_yieldable(
Expand Down
58 changes: 53 additions & 5 deletions synapse/handlers/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
import logging
from typing import TYPE_CHECKING, List, Optional, Tuple

from synapse.api.constants import ReadReceiptEventFields
from synapse.appservice import ApplicationService
from synapse.handlers._base import BaseHandler
from synapse.types import JsonDict, ReadReceipt, get_domain_from_id
from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -137,7 +138,7 @@ async def _handle_new_receipts(self, receipts: List[ReadReceipt]) -> bool:
return True

async def received_client_receipt(
self, room_id: str, receipt_type: str, user_id: str, event_id: str
self, room_id: str, receipt_type: str, user_id: str, event_id: str, hidden: bool
) -> None:
"""Called when a client tells us a local user has read up to the given
event_id in the room.
Expand All @@ -147,23 +148,67 @@ async def received_client_receipt(
receipt_type=receipt_type,
user_id=user_id,
event_ids=[event_id],
data={"ts": int(self.clock.time_msec())},
data={"ts": int(self.clock.time_msec()), "hidden": hidden},
)

is_new = await self._handle_new_receipts([receipt])
if not is_new:
return

if self.federation_sender:
if self.federation_sender and not (
self.hs.config.experimental.msc2285_enabled and hidden
):
await self.federation_sender.send_read_receipt(receipt)


class ReceiptEventSource:
def __init__(self, hs: "HomeServer"):
self.store = hs.get_datastore()
self.config = hs.config

@staticmethod
def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]:
visible_events = []

# filter out hidden receipts the user shouldn't see
for event in events:
content = event.get("content", {})
new_event = event.copy()
new_event["content"] = {}

for event_id in content.keys():
event_content = content.get(event_id, {})
m_read = event_content.get("m.read", {})

# If m_read is missing copy over the original event_content as there is nothing to process here
if not m_read:
new_event["content"][event_id] = event_content.copy()
continue

new_users = {}
for rr_user_id, user_rr in m_read.items():
hidden = user_rr.get("hidden", None)
if hidden is not True or rr_user_id == user_id:
new_users[rr_user_id] = user_rr.copy()
# If hidden has a value replace hidden with the correct prefixed key
if hidden is not None:
new_users[rr_user_id].pop("hidden")
new_users[rr_user_id][
ReadReceiptEventFields.MSC2285_HIDDEN
] = hidden

# Set new users unless empty
if len(new_users.keys()) > 0:
new_event["content"][event_id] = {"m.read": new_users}

# Append new_event to visible_events unless empty
if len(new_event["content"].keys()) > 0:
visible_events.append(new_event)

return visible_events

async def get_new_events(
self, from_key: int, room_ids: List[str], **kwargs
self, from_key: int, room_ids: List[str], user: UserID, **kwargs
) -> Tuple[List[JsonDict], int]:
from_key = int(from_key)
to_key = self.get_current_key()
Expand All @@ -175,6 +220,9 @@ async def get_new_events(
room_ids, from_key=from_key, to_key=to_key
)

if self.config.experimental.msc2285_enabled:
events = ReceiptEventSource.filter_out_hidden(events, user.to_string())

return (events, to_key)

async def get_new_events_as(
Expand Down
5 changes: 5 additions & 0 deletions synapse/replication/tcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,11 @@ async def _on_new_receipts(self, rows):
# we only want to send on receipts for our own users
if not self._is_mine_id(receipt.user_id):
continue
if (
receipt.data.get("hidden", False)
and self._hs.config.experimental.msc2285_enabled
):
continue
receipt_info = ReadReceipt(
receipt.room_id,
receipt.receipt_type,
Expand Down
14 changes: 13 additions & 1 deletion synapse/rest/client/v2_alpha/read_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import logging

from synapse.api.constants import ReadReceiptEventFields
from synapse.api.errors import Codes, SynapseError
from synapse.http.servlet import RestServlet, parse_json_object_from_request

from ._base import client_patterns
Expand All @@ -37,14 +39,24 @@ async def on_POST(self, request, room_id):
await self.presence_handler.bump_presence_active_time(requester.user)

body = parse_json_object_from_request(request)

read_event_id = body.get("m.read", None)
hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False)

if not isinstance(hidden, bool):
raise SynapseError(
400,
"Param %s must be a boolean, if given"
% ReadReceiptEventFields.MSC2285_HIDDEN,
Codes.BAD_JSON,
)

if read_event_id:
await self.receipts_handler.received_client_receipt(
room_id,
"m.read",
user_id=requester.user.to_string(),
event_id=read_event_id,
hidden=hidden,
)

read_marker_event_id = body.get("m.fully_read", None)
Expand Down
22 changes: 19 additions & 3 deletions synapse/rest/client/v2_alpha/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@

import logging

from synapse.api.errors import SynapseError
from synapse.http.servlet import RestServlet
from synapse.api.constants import ReadReceiptEventFields
from synapse.api.errors import Codes, SynapseError
from synapse.http.servlet import RestServlet, parse_json_object_from_request

from ._base import client_patterns

Expand All @@ -42,10 +43,25 @@ async def on_POST(self, request, room_id, receipt_type, event_id):
if receipt_type != "m.read":
raise SynapseError(400, "Receipt type must be 'm.read'")

body = parse_json_object_from_request(request)
hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False)

if not isinstance(hidden, bool):
raise SynapseError(
400,
"Param %s must be a boolean, if given"
% ReadReceiptEventFields.MSC2285_HIDDEN,
Codes.BAD_JSON,
)

await self.presence_handler.bump_presence_active_time(requester.user)

await self.receipts_handler.received_client_receipt(
room_id, receipt_type, user_id=requester.user.to_string(), event_id=event_id
room_id,
receipt_type,
user_id=requester.user.to_string(),
event_id=event_id,
hidden=hidden,
)

return 200, {}
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/client/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ def on_GET(self, request):
"io.element.e2ee_forced.trusted_private": self.e2ee_forced_trusted_private,
# Supports the busy presence state described in MSC3026.
"org.matrix.msc3026.busy_presence": self.config.experimental.msc3026_enabled,
# Supports receiving hidden read receipts as per MSC2285
"org.matrix.msc2285": self.config.experimental.msc2285_enabled,
},
},
)
Expand Down
Loading