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

Commit

Permalink
Support for MSC2285 (hidden read receipts) (#10413)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonBrandner authored Jul 28, 2021
1 parent 0489683 commit c3b0377
Show file tree
Hide file tree
Showing 11 changed files with 495 additions and 12 deletions.
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

0 comments on commit c3b0377

Please sign in to comment.