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 9 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).
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 4 additions & 2 deletions 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 @@ -126,6 +127,7 @@ async def _snapshot_all_rooms(
joined_rooms,
to_key=int(now_token.receipt_key),
)
receipt = ReceiptEventSource.filter_out_hidden(receipt, user_id)

tags_by_room = await self.store.get_tags_for_user(user_id)

Expand Down Expand Up @@ -422,8 +424,8 @@ async def get_receipts():
room_id, to_key=now_token.receipt_key
)
if not receipts:
receipts = []
return receipts
return []
return ReceiptEventSource.filter_out_hidden(receipts, user_id)

presence, receipts, (messages, token) = await make_deferred_yieldable(
defer.gatherResults(
Expand Down
53 changes: 47 additions & 6 deletions synapse/handlers/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

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 +137,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 +147,63 @@ 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 hidden:
await self.federation_sender.send_read_receipt(receipt)


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

@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 in m_read.keys():
user_rr = m_read[rr_user_id]
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
hidden = user_rr.get("hidden", False)
if not hidden or rr_user_id == user_id:
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
new_users[rr_user_id] = user_rr.copy()
# If hidden has a value replace hidden with the correct prefixed key
if user_rr.get("hidden", None) is not None:
new_users[rr_user_id].pop("hidden")
new_users[rr_user_id]["org.matrix.msc2285.hidden"] = hidden
babolivier marked this conversation as resolved.
Show resolved Hide resolved
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved

# 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 @@ -174,8 +214,9 @@ async def get_new_events(
events = await self.store.get_linearized_receipts_for_rooms(
room_ids, from_key=from_key, to_key=to_key
)
filtered_events = ReceiptEventSource.filter_out_hidden(events, user.to_string())

return (events, to_key)
return (filtered_events, to_key)

async def get_new_events_as(
self, from_key: int, service: ApplicationService
Expand Down
2 changes: 2 additions & 0 deletions synapse/replication/tcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,8 @@ 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):
continue
receipt_info = ReadReceipt(
receipt.room_id,
receipt.receipt_type,
Expand Down
13 changes: 12 additions & 1 deletion synapse/rest/client/v2_alpha/read_marker.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
# limitations under the License.

import logging
from http import HTTPStatus

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,23 @@ 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("org.matrix.msc2285.hidden", False)

if not isinstance(hidden, bool):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
"Param 'hidden' must be a boolean, if given",
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
21 changes: 18 additions & 3 deletions synapse/rest/client/v2_alpha/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
# limitations under the License.

import logging
from http import HTTPStatus

from synapse.api.errors import SynapseError
from synapse.http.servlet import RestServlet
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,24 @@ 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("org.matrix.msc2285.hidden", False)

if not isinstance(hidden, bool):
raise SynapseError(
HTTPStatus.BAD_REQUEST,
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
"Param 'hidden' must be a boolean, if given",
SimonBrandner marked this conversation as resolved.
Show resolved Hide resolved
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": True,
},
},
)
Expand Down
Loading