mirror of
https://mau.dev/maunium/synapse.git
synced 2025-01-06 02:24:01 +01:00
Implement changes to MSC2285 (hidden read receipts) (#12168)
* Changes hidden read receipts to be a separate receipt type (instead of a field on `m.read`). * Updates the `/receipts` endpoint to accept `m.fully_read`.
This commit is contained in:
parent
332cce8dcf
commit
116a4c8340
12 changed files with 648 additions and 187 deletions
1
changelog.d/12168.feature
Normal file
1
changelog.d/12168.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Implement [changes](https://github.com/matrix-org/matrix-spec-proposals/pull/2285/commits/4a77139249c2e830aec3c7d6bd5501a514d1cc27) to [MSC2285 (hidden read receipts)](https://github.com/matrix-org/matrix-spec-proposals/pull/2285). Contributed by @SimonBrandner.
|
|
@ -255,7 +255,5 @@ class GuestAccess:
|
||||||
|
|
||||||
class ReceiptTypes:
|
class ReceiptTypes:
|
||||||
READ: Final = "m.read"
|
READ: Final = "m.read"
|
||||||
|
READ_PRIVATE: Final = "org.matrix.msc2285.read.private"
|
||||||
|
FULLY_READ: Final = "m.fully_read"
|
||||||
class ReadReceiptEventFields:
|
|
||||||
MSC2285_HIDDEN: Final = "org.matrix.msc2285.hidden"
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
|
from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
from synapse.api.constants import ReadReceiptEventFields, ReceiptTypes
|
from synapse.api.constants import ReceiptTypes
|
||||||
from synapse.appservice import ApplicationService
|
from synapse.appservice import ApplicationService
|
||||||
from synapse.streams import EventSource
|
from synapse.streams import EventSource
|
||||||
from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id
|
from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id
|
||||||
|
@ -112,7 +112,7 @@ class ReceiptsHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
if not res:
|
if not res:
|
||||||
# res will be None if this read receipt is 'old'
|
# res will be None if this receipt is 'old'
|
||||||
continue
|
continue
|
||||||
|
|
||||||
stream_id, max_persisted_id = res
|
stream_id, max_persisted_id = res
|
||||||
|
@ -138,7 +138,7 @@ class ReceiptsHandler:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def received_client_receipt(
|
async def received_client_receipt(
|
||||||
self, room_id: str, receipt_type: str, user_id: str, event_id: str, hidden: bool
|
self, room_id: str, receipt_type: str, user_id: str, event_id: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Called when a client tells us a local user has read up to the given
|
"""Called when a client tells us a local user has read up to the given
|
||||||
event_id in the room.
|
event_id in the room.
|
||||||
|
@ -148,16 +148,14 @@ class ReceiptsHandler:
|
||||||
receipt_type=receipt_type,
|
receipt_type=receipt_type,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
event_ids=[event_id],
|
event_ids=[event_id],
|
||||||
data={"ts": int(self.clock.time_msec()), "hidden": hidden},
|
data={"ts": int(self.clock.time_msec())},
|
||||||
)
|
)
|
||||||
|
|
||||||
is_new = await self._handle_new_receipts([receipt])
|
is_new = await self._handle_new_receipts([receipt])
|
||||||
if not is_new:
|
if not is_new:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.federation_sender and not (
|
if self.federation_sender and receipt_type != ReceiptTypes.READ_PRIVATE:
|
||||||
self.hs.config.experimental.msc2285_enabled and hidden
|
|
||||||
):
|
|
||||||
await self.federation_sender.send_read_receipt(receipt)
|
await self.federation_sender.send_read_receipt(receipt)
|
||||||
|
|
||||||
|
|
||||||
|
@ -168,6 +166,13 @@ class ReceiptEventSource(EventSource[int, JsonDict]):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]:
|
def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]:
|
||||||
|
"""
|
||||||
|
This method takes in what is returned by
|
||||||
|
get_linearized_receipts_for_rooms() and goes through read receipts
|
||||||
|
filtering out m.read.private receipts if they were not sent by the
|
||||||
|
current user.
|
||||||
|
"""
|
||||||
|
|
||||||
visible_events = []
|
visible_events = []
|
||||||
|
|
||||||
# filter out hidden receipts the user shouldn't see
|
# filter out hidden receipts the user shouldn't see
|
||||||
|
@ -176,37 +181,21 @@ class ReceiptEventSource(EventSource[int, JsonDict]):
|
||||||
new_event = event.copy()
|
new_event = event.copy()
|
||||||
new_event["content"] = {}
|
new_event["content"] = {}
|
||||||
|
|
||||||
for event_id in content.keys():
|
for event_id, event_content in content.items():
|
||||||
event_content = content.get(event_id, {})
|
receipt_event = {}
|
||||||
m_read = event_content.get(ReceiptTypes.READ, {})
|
for receipt_type, receipt_content in event_content.items():
|
||||||
|
if receipt_type == ReceiptTypes.READ_PRIVATE:
|
||||||
|
user_rr = receipt_content.get(user_id, None)
|
||||||
|
if user_rr:
|
||||||
|
receipt_event[ReceiptTypes.READ_PRIVATE] = {
|
||||||
|
user_id: user_rr.copy()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
receipt_event[receipt_type] = receipt_content.copy()
|
||||||
|
|
||||||
# If m_read is missing copy over the original event_content as there is nothing to process here
|
# Only include the receipt event if it is non-empty.
|
||||||
if not m_read:
|
if receipt_event:
|
||||||
new_event["content"][event_id] = event_content.copy()
|
new_event["content"][event_id] = receipt_event
|
||||||
continue
|
|
||||||
|
|
||||||
new_users = {}
|
|
||||||
for rr_user_id, user_rr in m_read.items():
|
|
||||||
try:
|
|
||||||
hidden = user_rr.get("hidden")
|
|
||||||
except AttributeError:
|
|
||||||
# Due to https://github.com/matrix-org/synapse/issues/10376
|
|
||||||
# there are cases where user_rr is a string, in those cases
|
|
||||||
# we just ignore the read receipt
|
|
||||||
continue
|
|
||||||
|
|
||||||
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] = {ReceiptTypes.READ: new_users}
|
|
||||||
|
|
||||||
# Append new_event to visible_events unless empty
|
# Append new_event to visible_events unless empty
|
||||||
if len(new_event["content"].keys()) > 0:
|
if len(new_event["content"].keys()) > 0:
|
||||||
|
|
|
@ -1045,7 +1045,7 @@ class SyncHandler:
|
||||||
last_unread_event_id = await self.store.get_last_receipt_event_id_for_user(
|
last_unread_event_id = await self.store.get_last_receipt_event_id_for_user(
|
||||||
user_id=sync_config.user.to_string(),
|
user_id=sync_config.user.to_string(),
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
receipt_type=ReceiptTypes.READ,
|
receipt_types=(ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE),
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self.store.get_unread_event_push_actions_by_room_for_user(
|
return await self.store.get_unread_event_push_actions_by_room_for_user(
|
||||||
|
|
|
@ -24,7 +24,9 @@ async def get_badge_count(store: DataStore, user_id: str, group_by_room: bool) -
|
||||||
invites = await store.get_invited_rooms_for_local_user(user_id)
|
invites = await store.get_invited_rooms_for_local_user(user_id)
|
||||||
joins = await store.get_rooms_for_user(user_id)
|
joins = await store.get_rooms_for_user(user_id)
|
||||||
|
|
||||||
my_receipts_by_room = await store.get_receipts_for_user(user_id, ReceiptTypes.READ)
|
my_receipts_by_room = await store.get_receipts_for_user(
|
||||||
|
user_id, (ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE)
|
||||||
|
)
|
||||||
|
|
||||||
badge = len(invites)
|
badge = len(invites)
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ class NotificationsServlet(RestServlet):
|
||||||
)
|
)
|
||||||
|
|
||||||
receipts_by_room = await self.store.get_receipts_for_user_with_orderings(
|
receipts_by_room = await self.store.get_receipts_for_user_with_orderings(
|
||||||
user_id, ReceiptTypes.READ
|
user_id, [ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE]
|
||||||
)
|
)
|
||||||
|
|
||||||
notif_event_ids = [pa.event_id for pa in push_actions]
|
notif_event_ids = [pa.event_id for pa in push_actions]
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Tuple
|
from typing import TYPE_CHECKING, Tuple
|
||||||
|
|
||||||
from synapse.api.constants import ReadReceiptEventFields, ReceiptTypes
|
from synapse.api.constants import ReceiptTypes
|
||||||
from synapse.api.errors import Codes, SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.http.server import HttpServer
|
from synapse.http.server import HttpServer
|
||||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
|
@ -36,6 +36,7 @@ class ReadMarkerRestServlet(RestServlet):
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
|
self.config = hs.config
|
||||||
self.receipts_handler = hs.get_receipts_handler()
|
self.receipts_handler = hs.get_receipts_handler()
|
||||||
self.read_marker_handler = hs.get_read_marker_handler()
|
self.read_marker_handler = hs.get_read_marker_handler()
|
||||||
self.presence_handler = hs.get_presence_handler()
|
self.presence_handler = hs.get_presence_handler()
|
||||||
|
@ -48,27 +49,38 @@ class ReadMarkerRestServlet(RestServlet):
|
||||||
await self.presence_handler.bump_presence_active_time(requester.user)
|
await self.presence_handler.bump_presence_active_time(requester.user)
|
||||||
|
|
||||||
body = parse_json_object_from_request(request)
|
body = parse_json_object_from_request(request)
|
||||||
read_event_id = body.get(ReceiptTypes.READ, None)
|
|
||||||
hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False)
|
|
||||||
|
|
||||||
if not isinstance(hidden, bool):
|
valid_receipt_types = {ReceiptTypes.READ, ReceiptTypes.FULLY_READ}
|
||||||
|
if self.config.experimental.msc2285_enabled:
|
||||||
|
valid_receipt_types.add(ReceiptTypes.READ_PRIVATE)
|
||||||
|
|
||||||
|
if set(body.keys()) > valid_receipt_types:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400,
|
400,
|
||||||
"Param %s must be a boolean, if given"
|
"Receipt type must be 'm.read', 'org.matrix.msc2285.read.private' or 'm.fully_read'"
|
||||||
% ReadReceiptEventFields.MSC2285_HIDDEN,
|
if self.config.experimental.msc2285_enabled
|
||||||
Codes.BAD_JSON,
|
else "Receipt type must be 'm.read' or 'm.fully_read'",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
read_event_id = body.get(ReceiptTypes.READ, None)
|
||||||
if read_event_id:
|
if read_event_id:
|
||||||
await self.receipts_handler.received_client_receipt(
|
await self.receipts_handler.received_client_receipt(
|
||||||
room_id,
|
room_id,
|
||||||
ReceiptTypes.READ,
|
ReceiptTypes.READ,
|
||||||
user_id=requester.user.to_string(),
|
user_id=requester.user.to_string(),
|
||||||
event_id=read_event_id,
|
event_id=read_event_id,
|
||||||
hidden=hidden,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
read_marker_event_id = body.get("m.fully_read", None)
|
read_private_event_id = body.get(ReceiptTypes.READ_PRIVATE, None)
|
||||||
|
if read_private_event_id and self.config.experimental.msc2285_enabled:
|
||||||
|
await self.receipts_handler.received_client_receipt(
|
||||||
|
room_id,
|
||||||
|
ReceiptTypes.READ_PRIVATE,
|
||||||
|
user_id=requester.user.to_string(),
|
||||||
|
event_id=read_private_event_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
read_marker_event_id = body.get(ReceiptTypes.FULLY_READ, None)
|
||||||
if read_marker_event_id:
|
if read_marker_event_id:
|
||||||
await self.read_marker_handler.received_client_read_marker(
|
await self.read_marker_handler.received_client_read_marker(
|
||||||
room_id,
|
room_id,
|
||||||
|
|
|
@ -16,8 +16,8 @@ import logging
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, Tuple
|
from typing import TYPE_CHECKING, Tuple
|
||||||
|
|
||||||
from synapse.api.constants import ReadReceiptEventFields, ReceiptTypes
|
from synapse.api.constants import ReceiptTypes
|
||||||
from synapse.api.errors import Codes, SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.http import get_request_user_agent
|
from synapse.http import get_request_user_agent
|
||||||
from synapse.http.server import HttpServer
|
from synapse.http.server import HttpServer
|
||||||
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
from synapse.http.servlet import RestServlet, parse_json_object_from_request
|
||||||
|
@ -46,6 +46,7 @@ class ReceiptRestServlet(RestServlet):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.receipts_handler = hs.get_receipts_handler()
|
self.receipts_handler = hs.get_receipts_handler()
|
||||||
|
self.read_marker_handler = hs.get_read_marker_handler()
|
||||||
self.presence_handler = hs.get_presence_handler()
|
self.presence_handler = hs.get_presence_handler()
|
||||||
|
|
||||||
async def on_POST(
|
async def on_POST(
|
||||||
|
@ -53,7 +54,19 @@ class ReceiptRestServlet(RestServlet):
|
||||||
) -> Tuple[int, JsonDict]:
|
) -> Tuple[int, JsonDict]:
|
||||||
requester = await self.auth.get_user_by_req(request)
|
requester = await self.auth.get_user_by_req(request)
|
||||||
|
|
||||||
if receipt_type != ReceiptTypes.READ:
|
if self.hs.config.experimental.msc2285_enabled and receipt_type not in [
|
||||||
|
ReceiptTypes.READ,
|
||||||
|
ReceiptTypes.READ_PRIVATE,
|
||||||
|
ReceiptTypes.FULLY_READ,
|
||||||
|
]:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Receipt type must be 'm.read', 'org.matrix.msc2285.read.private' or 'm.fully_read'",
|
||||||
|
)
|
||||||
|
elif (
|
||||||
|
not self.hs.config.experimental.msc2285_enabled
|
||||||
|
and receipt_type != ReceiptTypes.READ
|
||||||
|
):
|
||||||
raise SynapseError(400, "Receipt type must be 'm.read'")
|
raise SynapseError(400, "Receipt type must be 'm.read'")
|
||||||
|
|
||||||
# Do not allow older SchildiChat and Element Android clients (prior to Element/1.[012].x) to send an empty body.
|
# Do not allow older SchildiChat and Element Android clients (prior to Element/1.[012].x) to send an empty body.
|
||||||
|
@ -62,26 +75,24 @@ class ReceiptRestServlet(RestServlet):
|
||||||
if "Android" in user_agent:
|
if "Android" in user_agent:
|
||||||
if pattern.match(user_agent) or "Riot" in user_agent:
|
if pattern.match(user_agent) or "Riot" in user_agent:
|
||||||
allow_empty_body = True
|
allow_empty_body = True
|
||||||
body = parse_json_object_from_request(request, allow_empty_body)
|
# This call makes sure possible empty body is handled correctly
|
||||||
hidden = body.get(ReadReceiptEventFields.MSC2285_HIDDEN, False)
|
parse_json_object_from_request(request, allow_empty_body)
|
||||||
|
|
||||||
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.presence_handler.bump_presence_active_time(requester.user)
|
||||||
|
|
||||||
await self.receipts_handler.received_client_receipt(
|
if receipt_type == ReceiptTypes.FULLY_READ:
|
||||||
room_id,
|
await self.read_marker_handler.received_client_read_marker(
|
||||||
receipt_type,
|
room_id,
|
||||||
user_id=requester.user.to_string(),
|
user_id=requester.user.to_string(),
|
||||||
event_id=event_id,
|
event_id=event_id,
|
||||||
hidden=hidden,
|
)
|
||||||
)
|
else:
|
||||||
|
await self.receipts_handler.received_client_receipt(
|
||||||
|
room_id,
|
||||||
|
receipt_type,
|
||||||
|
user_id=requester.user.to_string(),
|
||||||
|
event_id=event_id,
|
||||||
|
)
|
||||||
|
|
||||||
return 200, {}
|
return 200, {}
|
||||||
|
|
||||||
|
|
|
@ -144,12 +144,43 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
desc="get_receipts_for_room",
|
desc="get_receipts_for_room",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached()
|
|
||||||
async def get_last_receipt_event_id_for_user(
|
async def get_last_receipt_event_id_for_user(
|
||||||
self, user_id: str, room_id: str, receipt_type: str
|
self, user_id: str, room_id: str, receipt_types: Iterable[str]
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Fetch the event ID for the latest receipt in a room with the given receipt type.
|
Fetch the event ID for the latest receipt in a room with one of the given receipt types.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user to fetch receipts for.
|
||||||
|
room_id: The room ID to fetch the receipt for.
|
||||||
|
receipt_type: The receipt types to fetch. Earlier receipt types
|
||||||
|
are given priority if multiple receipts point to the same event.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The latest receipt, if one exists.
|
||||||
|
"""
|
||||||
|
latest_event_id: Optional[str] = None
|
||||||
|
latest_stream_ordering = 0
|
||||||
|
for receipt_type in receipt_types:
|
||||||
|
result = await self._get_last_receipt_event_id_for_user(
|
||||||
|
user_id, room_id, receipt_type
|
||||||
|
)
|
||||||
|
if result is None:
|
||||||
|
continue
|
||||||
|
event_id, stream_ordering = result
|
||||||
|
|
||||||
|
if latest_event_id is None or latest_stream_ordering < stream_ordering:
|
||||||
|
latest_event_id = event_id
|
||||||
|
latest_stream_ordering = stream_ordering
|
||||||
|
|
||||||
|
return latest_event_id
|
||||||
|
|
||||||
|
@cached()
|
||||||
|
async def _get_last_receipt_event_id_for_user(
|
||||||
|
self, user_id: str, room_id: str, receipt_type: str
|
||||||
|
) -> Optional[Tuple[str, int]]:
|
||||||
|
"""
|
||||||
|
Fetch the event ID and stream ordering for the latest receipt.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: The user to fetch receipts for.
|
user_id: The user to fetch receipts for.
|
||||||
|
@ -157,30 +188,33 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
receipt_type: The receipt type to fetch.
|
receipt_type: The receipt type to fetch.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The event ID of the latest receipt, if one exists; otherwise `None`.
|
The event ID and stream ordering of the latest receipt, if one exists;
|
||||||
|
otherwise `None`.
|
||||||
|
"""
|
||||||
|
sql = """
|
||||||
|
SELECT event_id, stream_ordering
|
||||||
|
FROM receipts_linearized
|
||||||
|
INNER JOIN events USING (room_id, event_id)
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND room_id = ?
|
||||||
|
AND receipt_type = ?
|
||||||
"""
|
"""
|
||||||
return await self.db_pool.simple_select_one_onecol(
|
|
||||||
table="receipts_linearized",
|
|
||||||
keyvalues={
|
|
||||||
"room_id": room_id,
|
|
||||||
"receipt_type": receipt_type,
|
|
||||||
"user_id": user_id,
|
|
||||||
},
|
|
||||||
retcol="event_id",
|
|
||||||
desc="get_own_receipt_for_user",
|
|
||||||
allow_none=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@cached()
|
def f(txn: LoggingTransaction) -> Optional[Tuple[str, int]]:
|
||||||
|
txn.execute(sql, (user_id, room_id, receipt_type))
|
||||||
|
return cast(Optional[Tuple[str, int]], txn.fetchone())
|
||||||
|
|
||||||
|
return await self.db_pool.runInteraction("get_own_receipt_for_user", f)
|
||||||
|
|
||||||
async def get_receipts_for_user(
|
async def get_receipts_for_user(
|
||||||
self, user_id: str, receipt_type: str
|
self, user_id: str, receipt_types: Iterable[str]
|
||||||
) -> Dict[str, str]:
|
) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Fetch the event IDs for the latest receipts sent by the given user.
|
Fetch the event IDs for the latest receipts sent by the given user.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: The user to fetch receipts for.
|
user_id: The user to fetch receipts for.
|
||||||
receipt_type: The receipt type to fetch.
|
receipt_types: The receipt types to check.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A map of room ID to the event ID of the latest receipt for that room.
|
A map of room ID to the event ID of the latest receipt for that room.
|
||||||
|
@ -188,16 +222,48 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
If the user has not sent a receipt to a room then it will not appear
|
If the user has not sent a receipt to a room then it will not appear
|
||||||
in the returned dictionary.
|
in the returned dictionary.
|
||||||
"""
|
"""
|
||||||
rows = await self.db_pool.simple_select_list(
|
results = await self.get_receipts_for_user_with_orderings(
|
||||||
table="receipts_linearized",
|
user_id, receipt_types
|
||||||
keyvalues={"user_id": user_id, "receipt_type": receipt_type},
|
|
||||||
retcols=("room_id", "event_id"),
|
|
||||||
desc="get_receipts_for_user",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return {row["room_id"]: row["event_id"] for row in rows}
|
# Reduce the result to room ID -> event ID.
|
||||||
|
return {
|
||||||
|
room_id: room_result["event_id"] for room_id, room_result in results.items()
|
||||||
|
}
|
||||||
|
|
||||||
async def get_receipts_for_user_with_orderings(
|
async def get_receipts_for_user_with_orderings(
|
||||||
|
self, user_id: str, receipt_types: Iterable[str]
|
||||||
|
) -> JsonDict:
|
||||||
|
"""
|
||||||
|
Fetch receipts for all rooms that the given user is joined to.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user to fetch receipts for.
|
||||||
|
receipt_types: The receipt types to fetch. Earlier receipt types
|
||||||
|
are given priority if multiple receipts point to the same event.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A map of room ID to the latest receipt (for the given types).
|
||||||
|
"""
|
||||||
|
results: JsonDict = {}
|
||||||
|
for receipt_type in receipt_types:
|
||||||
|
partial_result = await self._get_receipts_for_user_with_orderings(
|
||||||
|
user_id, receipt_type
|
||||||
|
)
|
||||||
|
for room_id, room_result in partial_result.items():
|
||||||
|
# If the room has not yet been seen, or the receipt is newer,
|
||||||
|
# use it.
|
||||||
|
if (
|
||||||
|
room_id not in results
|
||||||
|
or results[room_id]["stream_ordering"]
|
||||||
|
< room_result["stream_ordering"]
|
||||||
|
):
|
||||||
|
results[room_id] = room_result
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@cached()
|
||||||
|
async def _get_receipts_for_user_with_orderings(
|
||||||
self, user_id: str, receipt_type: str
|
self, user_id: str, receipt_type: str
|
||||||
) -> JsonDict:
|
) -> JsonDict:
|
||||||
"""
|
"""
|
||||||
|
@ -220,8 +286,9 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
" WHERE rl.room_id = e.room_id"
|
" WHERE rl.room_id = e.room_id"
|
||||||
" AND rl.event_id = e.event_id"
|
" AND rl.event_id = e.event_id"
|
||||||
" AND user_id = ?"
|
" AND user_id = ?"
|
||||||
|
" AND receipt_type = ?"
|
||||||
)
|
)
|
||||||
txn.execute(sql, (user_id,))
|
txn.execute(sql, (user_id, receipt_type))
|
||||||
return cast(List[Tuple[str, str, int, int]], txn.fetchall())
|
return cast(List[Tuple[str, str, int, int]], txn.fetchall())
|
||||||
|
|
||||||
rows = await self.db_pool.runInteraction(
|
rows = await self.db_pool.runInteraction(
|
||||||
|
@ -552,9 +619,9 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
def invalidate_caches_for_receipt(
|
def invalidate_caches_for_receipt(
|
||||||
self, room_id: str, receipt_type: str, user_id: str
|
self, room_id: str, receipt_type: str, user_id: str
|
||||||
) -> None:
|
) -> None:
|
||||||
self.get_receipts_for_user.invalidate((user_id, receipt_type))
|
self._get_receipts_for_user_with_orderings.invalidate((user_id, receipt_type))
|
||||||
self._get_linearized_receipts_for_room.invalidate((room_id,))
|
self._get_linearized_receipts_for_room.invalidate((room_id,))
|
||||||
self.get_last_receipt_event_id_for_user.invalidate(
|
self._get_last_receipt_event_id_for_user.invalidate(
|
||||||
(user_id, room_id, receipt_type)
|
(user_id, room_id, receipt_type)
|
||||||
)
|
)
|
||||||
self._invalidate_get_users_with_receipts_in_room(room_id, receipt_type, user_id)
|
self._invalidate_get_users_with_receipts_in_room(room_id, receipt_type, user_id)
|
||||||
|
@ -590,8 +657,8 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
"""Inserts a receipt into the database if it's newer than the current one.
|
"""Inserts a receipt into the database if it's newer than the current one.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None if the RR is older than the current RR
|
None if the receipt is older than the current receipt
|
||||||
otherwise, the rx timestamp of the event that the RR corresponds to
|
otherwise, the rx timestamp of the event that the receipt corresponds to
|
||||||
(or 0 if the event is unknown)
|
(or 0 if the event is unknown)
|
||||||
"""
|
"""
|
||||||
assert self._can_write_to_receipts
|
assert self._can_write_to_receipts
|
||||||
|
@ -612,7 +679,7 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
if stream_ordering is not None:
|
if stream_ordering is not None:
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT stream_ordering, event_id FROM events"
|
"SELECT stream_ordering, event_id FROM events"
|
||||||
" INNER JOIN receipts_linearized as r USING (event_id, room_id)"
|
" INNER JOIN receipts_linearized AS r USING (event_id, room_id)"
|
||||||
" WHERE r.room_id = ? AND r.receipt_type = ? AND r.user_id = ?"
|
" WHERE r.room_id = ? AND r.receipt_type = ? AND r.user_id = ?"
|
||||||
)
|
)
|
||||||
txn.execute(sql, (room_id, receipt_type, user_id))
|
txn.execute(sql, (room_id, receipt_type, user_id))
|
||||||
|
@ -653,7 +720,10 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
lock=False,
|
lock=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if receipt_type == ReceiptTypes.READ and stream_ordering is not None:
|
if (
|
||||||
|
receipt_type in (ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE)
|
||||||
|
and stream_ordering is not None
|
||||||
|
):
|
||||||
self._remove_old_push_actions_before_txn( # type: ignore[attr-defined]
|
self._remove_old_push_actions_before_txn( # type: ignore[attr-defined]
|
||||||
txn, room_id=room_id, user_id=user_id, stream_ordering=stream_ordering
|
txn, room_id=room_id, user_id=user_id, stream_ordering=stream_ordering
|
||||||
)
|
)
|
||||||
|
@ -672,6 +742,10 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
|
|
||||||
Automatically does conversion between linearized and graph
|
Automatically does conversion between linearized and graph
|
||||||
representations.
|
representations.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The new receipts stream ID and token, if the receipt is newer than
|
||||||
|
what was previously persisted. None, otherwise.
|
||||||
"""
|
"""
|
||||||
assert self._can_write_to_receipts
|
assert self._can_write_to_receipts
|
||||||
|
|
||||||
|
@ -719,6 +793,7 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If the receipt was older than the currently persisted one, nothing to do.
|
||||||
if event_ts is None:
|
if event_ts is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -774,7 +849,10 @@ class ReceiptsWorkerStore(SQLBaseStore):
|
||||||
receipt_type,
|
receipt_type,
|
||||||
user_id,
|
user_id,
|
||||||
)
|
)
|
||||||
txn.call_after(self.get_receipts_for_user.invalidate, (user_id, receipt_type))
|
txn.call_after(
|
||||||
|
self._get_receipts_for_user_with_orderings.invalidate,
|
||||||
|
(user_id, receipt_type),
|
||||||
|
)
|
||||||
# FIXME: This shouldn't invalidate the whole cache
|
# FIXME: This shouldn't invalidate the whole cache
|
||||||
txn.call_after(self._get_linearized_receipts_for_room.invalidate, (room_id,))
|
txn.call_after(self._get_linearized_receipts_for_room.invalidate, (room_id,))
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from synapse.api.constants import ReadReceiptEventFields, ReceiptTypes
|
from synapse.api.constants import ReceiptTypes
|
||||||
from synapse.types import JsonDict
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
@ -25,20 +25,15 @@ class ReceiptsTestCase(unittest.HomeserverTestCase):
|
||||||
def prepare(self, reactor, clock, hs):
|
def prepare(self, reactor, clock, hs):
|
||||||
self.event_source = hs.get_event_sources().sources.receipt
|
self.event_source = hs.get_event_sources().sources.receipt
|
||||||
|
|
||||||
# In the first param of _test_filters_hidden we use "hidden" instead of
|
|
||||||
# ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking
|
|
||||||
# the data from the database which doesn't use the prefix
|
|
||||||
|
|
||||||
def test_filters_out_hidden_receipt(self):
|
def test_filters_out_hidden_receipt(self):
|
||||||
self._test_filters_hidden(
|
self._test_filters_hidden(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"content": {
|
"content": {
|
||||||
"$1435641916114394fHBLK:matrix.org": {
|
"$1435641916114394fHBLK:matrix.org": {
|
||||||
ReceiptTypes.READ: {
|
ReceiptTypes.READ_PRIVATE: {
|
||||||
"@rikj:jki.re": {
|
"@rikj:jki.re": {
|
||||||
"ts": 1436451550453,
|
"ts": 1436451550453,
|
||||||
"hidden": True,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,58 +45,23 @@ class ReceiptsTestCase(unittest.HomeserverTestCase):
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_does_not_filter_out_our_hidden_receipt(self):
|
|
||||||
self._test_filters_hidden(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"content": {
|
|
||||||
"$1435641916hfgh4394fHBLK:matrix.org": {
|
|
||||||
ReceiptTypes.READ: {
|
|
||||||
"@me:server.org": {
|
|
||||||
"ts": 1436451550453,
|
|
||||||
"hidden": True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
|
||||||
"type": "m.receipt",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"content": {
|
|
||||||
"$1435641916hfgh4394fHBLK:matrix.org": {
|
|
||||||
ReceiptTypes.READ: {
|
|
||||||
"@me:server.org": {
|
|
||||||
"ts": 1436451550453,
|
|
||||||
ReadReceiptEventFields.MSC2285_HIDDEN: True,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
|
||||||
"type": "m.receipt",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_filters_out_hidden_receipt_and_ignores_rest(self):
|
def test_filters_out_hidden_receipt_and_ignores_rest(self):
|
||||||
self._test_filters_hidden(
|
self._test_filters_hidden(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"content": {
|
"content": {
|
||||||
"$1dgdgrd5641916114394fHBLK:matrix.org": {
|
"$1dgdgrd5641916114394fHBLK:matrix.org": {
|
||||||
ReceiptTypes.READ: {
|
ReceiptTypes.READ_PRIVATE: {
|
||||||
"@rikj:jki.re": {
|
"@rikj:jki.re": {
|
||||||
"ts": 1436451550453,
|
"ts": 1436451550453,
|
||||||
"hidden": True,
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
ReceiptTypes.READ: {
|
||||||
"@user:jki.re": {
|
"@user:jki.re": {
|
||||||
"ts": 1436451550453,
|
"ts": 1436451550453,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
"type": "m.receipt",
|
"type": "m.receipt",
|
||||||
|
@ -130,10 +90,9 @@ class ReceiptsTestCase(unittest.HomeserverTestCase):
|
||||||
{
|
{
|
||||||
"content": {
|
"content": {
|
||||||
"$14356419edgd14394fHBLK:matrix.org": {
|
"$14356419edgd14394fHBLK:matrix.org": {
|
||||||
ReceiptTypes.READ: {
|
ReceiptTypes.READ_PRIVATE: {
|
||||||
"@rikj:jki.re": {
|
"@rikj:jki.re": {
|
||||||
"ts": 1436451550453,
|
"ts": 1436451550453,
|
||||||
"hidden": True,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -223,7 +182,6 @@ class ReceiptsTestCase(unittest.HomeserverTestCase):
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"content": {
|
"content": {
|
||||||
"$143564gdfg6114394fHBLK:matrix.org": {},
|
|
||||||
"$1435641916114394fHBLK:matrix.org": {
|
"$1435641916114394fHBLK:matrix.org": {
|
||||||
ReceiptTypes.READ: {
|
ReceiptTypes.READ: {
|
||||||
"@user:jki.re": {
|
"@user:jki.re": {
|
||||||
|
@ -244,10 +202,9 @@ class ReceiptsTestCase(unittest.HomeserverTestCase):
|
||||||
{
|
{
|
||||||
"content": {
|
"content": {
|
||||||
"$14356419edgd14394fHBLK:matrix.org": {
|
"$14356419edgd14394fHBLK:matrix.org": {
|
||||||
ReceiptTypes.READ: {
|
ReceiptTypes.READ_PRIVATE: {
|
||||||
"@rikj:jki.re": {
|
"@rikj:jki.re": {
|
||||||
"ts": 1436451550453,
|
"ts": 1436451550453,
|
||||||
"hidden": True,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -306,7 +263,73 @@ class ReceiptsTestCase(unittest.HomeserverTestCase):
|
||||||
"type": "m.receipt",
|
"type": "m.receipt",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$14356419edgd14394fHBLK:matrix.org": {
|
||||||
|
ReceiptTypes.READ: {
|
||||||
|
"@rikj:jki.re": "string",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"type": "m.receipt",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_leaves_our_hidden_and_their_public(self):
|
||||||
|
self._test_filters_hidden(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$1dgdgrd5641916114394fHBLK:matrix.org": {
|
||||||
|
ReceiptTypes.READ_PRIVATE: {
|
||||||
|
"@me:server.org": {
|
||||||
|
"ts": 1436451550453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReceiptTypes.READ: {
|
||||||
|
"@rikj:jki.re": {
|
||||||
|
"ts": 1436451550453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"a.receipt.type": {
|
||||||
|
"@rikj:jki.re": {
|
||||||
|
"ts": 1436451550453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"type": "m.receipt",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"content": {
|
||||||
|
"$1dgdgrd5641916114394fHBLK:matrix.org": {
|
||||||
|
ReceiptTypes.READ_PRIVATE: {
|
||||||
|
"@me:server.org": {
|
||||||
|
"ts": 1436451550453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ReceiptTypes.READ: {
|
||||||
|
"@rikj:jki.re": {
|
||||||
|
"ts": 1436451550453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"a.receipt.type": {
|
||||||
|
"@rikj:jki.re": {
|
||||||
|
"ts": 1436451550453,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||||
|
"type": "m.receipt",
|
||||||
|
}
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def _test_filters_hidden(
|
def _test_filters_hidden(
|
||||||
|
|
|
@ -14,26 +14,246 @@
|
||||||
|
|
||||||
from synapse.api.constants import ReceiptTypes
|
from synapse.api.constants import ReceiptTypes
|
||||||
from synapse.replication.slave.storage.receipts import SlavedReceiptsStore
|
from synapse.replication.slave.storage.receipts import SlavedReceiptsStore
|
||||||
|
from synapse.types import UserID, create_requester
|
||||||
|
|
||||||
|
from tests.test_utils.event_injection import create_event
|
||||||
|
|
||||||
from ._base import BaseSlavedStoreTestCase
|
from ._base import BaseSlavedStoreTestCase
|
||||||
|
|
||||||
USER_ID = "@feeling:blue"
|
OTHER_USER_ID = "@other:test"
|
||||||
ROOM_ID = "!room:blue"
|
OUR_USER_ID = "@our:test"
|
||||||
EVENT_ID = "$event:blue"
|
|
||||||
|
|
||||||
|
|
||||||
class SlavedReceiptTestCase(BaseSlavedStoreTestCase):
|
class SlavedReceiptTestCase(BaseSlavedStoreTestCase):
|
||||||
|
|
||||||
STORE_TYPE = SlavedReceiptsStore
|
STORE_TYPE = SlavedReceiptsStore
|
||||||
|
|
||||||
def test_receipt(self):
|
def prepare(self, reactor, clock, homeserver):
|
||||||
self.check("get_receipts_for_user", [USER_ID, ReceiptTypes.READ], {})
|
super().prepare(reactor, clock, homeserver)
|
||||||
self.get_success(
|
self.room_creator = homeserver.get_room_creation_handler()
|
||||||
self.master_store.insert_receipt(
|
self.persist_event_storage = self.hs.get_storage().persistence
|
||||||
ROOM_ID, ReceiptTypes.READ, USER_ID, [EVENT_ID], {}
|
|
||||||
|
# Create a test user
|
||||||
|
self.ourUser = UserID.from_string(OUR_USER_ID)
|
||||||
|
self.ourRequester = create_requester(self.ourUser)
|
||||||
|
|
||||||
|
# Create a second test user
|
||||||
|
self.otherUser = UserID.from_string(OTHER_USER_ID)
|
||||||
|
self.otherRequester = create_requester(self.otherUser)
|
||||||
|
|
||||||
|
# Create a test room
|
||||||
|
info, _ = self.get_success(self.room_creator.create_room(self.ourRequester, {}))
|
||||||
|
self.room_id1 = info["room_id"]
|
||||||
|
|
||||||
|
# Create a second test room
|
||||||
|
info, _ = self.get_success(self.room_creator.create_room(self.ourRequester, {}))
|
||||||
|
self.room_id2 = info["room_id"]
|
||||||
|
|
||||||
|
# Join the second user to the first room
|
||||||
|
memberEvent, memberEventContext = self.get_success(
|
||||||
|
create_event(
|
||||||
|
self.hs,
|
||||||
|
room_id=self.room_id1,
|
||||||
|
type="m.room.member",
|
||||||
|
sender=self.otherRequester.user.to_string(),
|
||||||
|
state_key=self.otherRequester.user.to_string(),
|
||||||
|
content={"membership": "join"},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.replicate()
|
self.get_success(
|
||||||
self.check(
|
self.persist_event_storage.persist_event(memberEvent, memberEventContext)
|
||||||
"get_receipts_for_user", [USER_ID, ReceiptTypes.READ], {ROOM_ID: EVENT_ID}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Join the second user to the second room
|
||||||
|
memberEvent, memberEventContext = self.get_success(
|
||||||
|
create_event(
|
||||||
|
self.hs,
|
||||||
|
room_id=self.room_id2,
|
||||||
|
type="m.room.member",
|
||||||
|
sender=self.otherRequester.user.to_string(),
|
||||||
|
state_key=self.otherRequester.user.to_string(),
|
||||||
|
content={"membership": "join"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.get_success(
|
||||||
|
self.persist_event_storage.persist_event(memberEvent, memberEventContext)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_return_empty_with_no_data(self):
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_receipts_for_user(
|
||||||
|
OUR_USER_ID, [ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, {})
|
||||||
|
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_receipts_for_user_with_orderings(
|
||||||
|
OUR_USER_ID,
|
||||||
|
[ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, {})
|
||||||
|
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_last_receipt_event_id_for_user(
|
||||||
|
OUR_USER_ID,
|
||||||
|
self.room_id1,
|
||||||
|
[ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, None)
|
||||||
|
|
||||||
|
def test_get_receipts_for_user(self):
|
||||||
|
# Send some events into the first room
|
||||||
|
event1_1_id = self.create_and_send_event(
|
||||||
|
self.room_id1, UserID.from_string(OTHER_USER_ID)
|
||||||
|
)
|
||||||
|
event1_2_id = self.create_and_send_event(
|
||||||
|
self.room_id1, UserID.from_string(OTHER_USER_ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send public read receipt for the first event
|
||||||
|
self.get_success(
|
||||||
|
self.master_store.insert_receipt(
|
||||||
|
self.room_id1, ReceiptTypes.READ, OUR_USER_ID, [event1_1_id], {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Send private read receipt for the second event
|
||||||
|
self.get_success(
|
||||||
|
self.master_store.insert_receipt(
|
||||||
|
self.room_id1, ReceiptTypes.READ_PRIVATE, OUR_USER_ID, [event1_2_id], {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test we get the latest event when we want both private and public receipts
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_receipts_for_user(
|
||||||
|
OUR_USER_ID, [ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, {self.room_id1: event1_2_id})
|
||||||
|
|
||||||
|
# Test we get the older event when we want only public receipt
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_receipts_for_user(OUR_USER_ID, [ReceiptTypes.READ])
|
||||||
|
)
|
||||||
|
self.assertEqual(res, {self.room_id1: event1_1_id})
|
||||||
|
|
||||||
|
# Test we get the latest event when we want only the public receipt
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_receipts_for_user(
|
||||||
|
OUR_USER_ID, [ReceiptTypes.READ_PRIVATE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, {self.room_id1: event1_2_id})
|
||||||
|
|
||||||
|
# Test receipt updating
|
||||||
|
self.get_success(
|
||||||
|
self.master_store.insert_receipt(
|
||||||
|
self.room_id1, ReceiptTypes.READ, OUR_USER_ID, [event1_2_id], {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_receipts_for_user(OUR_USER_ID, [ReceiptTypes.READ])
|
||||||
|
)
|
||||||
|
self.assertEqual(res, {self.room_id1: event1_2_id})
|
||||||
|
|
||||||
|
# Send some events into the second room
|
||||||
|
event2_1_id = self.create_and_send_event(
|
||||||
|
self.room_id2, UserID.from_string(OTHER_USER_ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test new room is reflected in what the method returns
|
||||||
|
self.get_success(
|
||||||
|
self.master_store.insert_receipt(
|
||||||
|
self.room_id2, ReceiptTypes.READ_PRIVATE, OUR_USER_ID, [event2_1_id], {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_receipts_for_user(
|
||||||
|
OUR_USER_ID, [ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, {self.room_id1: event1_2_id, self.room_id2: event2_1_id})
|
||||||
|
|
||||||
|
def test_get_last_receipt_event_id_for_user(self):
|
||||||
|
# Send some events into the first room
|
||||||
|
event1_1_id = self.create_and_send_event(
|
||||||
|
self.room_id1, UserID.from_string(OTHER_USER_ID)
|
||||||
|
)
|
||||||
|
event1_2_id = self.create_and_send_event(
|
||||||
|
self.room_id1, UserID.from_string(OTHER_USER_ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send public read receipt for the first event
|
||||||
|
self.get_success(
|
||||||
|
self.master_store.insert_receipt(
|
||||||
|
self.room_id1, ReceiptTypes.READ, OUR_USER_ID, [event1_1_id], {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Send private read receipt for the second event
|
||||||
|
self.get_success(
|
||||||
|
self.master_store.insert_receipt(
|
||||||
|
self.room_id1, ReceiptTypes.READ_PRIVATE, OUR_USER_ID, [event1_2_id], {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test we get the latest event when we want both private and public receipts
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_last_receipt_event_id_for_user(
|
||||||
|
OUR_USER_ID,
|
||||||
|
self.room_id1,
|
||||||
|
[ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, event1_2_id)
|
||||||
|
|
||||||
|
# Test we get the older event when we want only public receipt
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_last_receipt_event_id_for_user(
|
||||||
|
OUR_USER_ID, self.room_id1, [ReceiptTypes.READ]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, event1_1_id)
|
||||||
|
|
||||||
|
# Test we get the latest event when we want only the private receipt
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_last_receipt_event_id_for_user(
|
||||||
|
OUR_USER_ID, self.room_id1, [ReceiptTypes.READ_PRIVATE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, event1_2_id)
|
||||||
|
|
||||||
|
# Test receipt updating
|
||||||
|
self.get_success(
|
||||||
|
self.master_store.insert_receipt(
|
||||||
|
self.room_id1, ReceiptTypes.READ, OUR_USER_ID, [event1_2_id], {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_last_receipt_event_id_for_user(
|
||||||
|
OUR_USER_ID, self.room_id1, [ReceiptTypes.READ]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, event1_2_id)
|
||||||
|
|
||||||
|
# Send some events into the second room
|
||||||
|
event2_1_id = self.create_and_send_event(
|
||||||
|
self.room_id2, UserID.from_string(OTHER_USER_ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test new room is reflected in what the method returns
|
||||||
|
self.get_success(
|
||||||
|
self.master_store.insert_receipt(
|
||||||
|
self.room_id2, ReceiptTypes.READ_PRIVATE, OUR_USER_ID, [event2_1_id], {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
res = self.get_success(
|
||||||
|
self.master_store.get_last_receipt_event_id_for_user(
|
||||||
|
OUR_USER_ID,
|
||||||
|
self.room_id2,
|
||||||
|
[ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(res, event2_1_id)
|
||||||
|
|
|
@ -23,7 +23,6 @@ import synapse.rest.admin
|
||||||
from synapse.api.constants import (
|
from synapse.api.constants import (
|
||||||
EventContentFields,
|
EventContentFields,
|
||||||
EventTypes,
|
EventTypes,
|
||||||
ReadReceiptEventFields,
|
|
||||||
ReceiptTypes,
|
ReceiptTypes,
|
||||||
RelationTypes,
|
RelationTypes,
|
||||||
)
|
)
|
||||||
|
@ -347,7 +346,7 @@ class SyncKnockTestCase(
|
||||||
# Knock on a room
|
# Knock on a room
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"POST",
|
"POST",
|
||||||
"/_matrix/client/r0/knock/%s" % (self.room_id,),
|
f"/_matrix/client/r0/knock/{self.room_id}",
|
||||||
b"{}",
|
b"{}",
|
||||||
self.knocker_tok,
|
self.knocker_tok,
|
||||||
)
|
)
|
||||||
|
@ -412,18 +411,79 @@ class ReadReceiptsTestCase(unittest.HomeserverTestCase):
|
||||||
# Send a message as the first user
|
# Send a message as the first user
|
||||||
res = self.helper.send(self.room_id, body="hello", tok=self.tok)
|
res = self.helper.send(self.room_id, body="hello", tok=self.tok)
|
||||||
|
|
||||||
# Send a read receipt to tell the server the first user's message was read
|
# Send a private read receipt to tell the server the first user's message was read
|
||||||
body = json.dumps({ReadReceiptEventFields.MSC2285_HIDDEN: True}).encode("utf8")
|
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"POST",
|
"POST",
|
||||||
"/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]),
|
f"/rooms/{self.room_id}/receipt/org.matrix.msc2285.read.private/{res['event_id']}",
|
||||||
body,
|
{},
|
||||||
access_token=self.tok2,
|
access_token=self.tok2,
|
||||||
)
|
)
|
||||||
self.assertEqual(channel.code, 200)
|
self.assertEqual(channel.code, 200)
|
||||||
|
|
||||||
# Test that the first user can't see the other user's hidden read receipt
|
# Test that the first user can't see the other user's private read receipt
|
||||||
self.assertEqual(self._get_read_receipt(), None)
|
self.assertIsNone(self._get_read_receipt())
|
||||||
|
|
||||||
|
@override_config({"experimental_features": {"msc2285_enabled": True}})
|
||||||
|
def test_public_receipt_can_override_private(self) -> None:
|
||||||
|
"""
|
||||||
|
Sending a public read receipt to the same event which has a private read
|
||||||
|
receipt should cause that receipt to become public.
|
||||||
|
"""
|
||||||
|
# Send a message as the first user
|
||||||
|
res = self.helper.send(self.room_id, body="hello", tok=self.tok)
|
||||||
|
|
||||||
|
# Send a private read receipt
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{self.room_id}/receipt/{ReceiptTypes.READ_PRIVATE}/{res['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=self.tok2,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200)
|
||||||
|
self.assertIsNone(self._get_read_receipt())
|
||||||
|
|
||||||
|
# Send a public read receipt
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{self.room_id}/receipt/{ReceiptTypes.READ}/{res['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=self.tok2,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200)
|
||||||
|
|
||||||
|
# Test that we did override the private read receipt
|
||||||
|
self.assertNotEqual(self._get_read_receipt(), None)
|
||||||
|
|
||||||
|
@override_config({"experimental_features": {"msc2285_enabled": True}})
|
||||||
|
def test_private_receipt_cannot_override_public(self) -> None:
|
||||||
|
"""
|
||||||
|
Sending a private read receipt to the same event which has a public read
|
||||||
|
receipt should cause no change.
|
||||||
|
"""
|
||||||
|
# Send a message as the first user
|
||||||
|
res = self.helper.send(self.room_id, body="hello", tok=self.tok)
|
||||||
|
|
||||||
|
# Send a public read receipt
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{self.room_id}/receipt/{ReceiptTypes.READ}/{res['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=self.tok2,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200)
|
||||||
|
self.assertNotEqual(self._get_read_receipt(), None)
|
||||||
|
|
||||||
|
# Send a private read receipt
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{self.room_id}/receipt/{ReceiptTypes.READ_PRIVATE}/{res['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=self.tok2,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200)
|
||||||
|
|
||||||
|
# Test that we didn't override the public read receipt
|
||||||
|
self.assertIsNone(self._get_read_receipt())
|
||||||
|
|
||||||
@parameterized.expand(
|
@parameterized.expand(
|
||||||
[
|
[
|
||||||
|
@ -455,7 +515,7 @@ class ReadReceiptsTestCase(unittest.HomeserverTestCase):
|
||||||
# Send a read receipt for this message with an empty body
|
# Send a read receipt for this message with an empty body
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"POST",
|
"POST",
|
||||||
"/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]),
|
f"/rooms/{self.room_id}/receipt/m.read/{res['event_id']}",
|
||||||
access_token=self.tok2,
|
access_token=self.tok2,
|
||||||
custom_headers=[("User-Agent", user_agent)],
|
custom_headers=[("User-Agent", user_agent)],
|
||||||
)
|
)
|
||||||
|
@ -479,6 +539,9 @@ class ReadReceiptsTestCase(unittest.HomeserverTestCase):
|
||||||
# Store the next batch for the next request.
|
# Store the next batch for the next request.
|
||||||
self.next_batch = channel.json_body["next_batch"]
|
self.next_batch = channel.json_body["next_batch"]
|
||||||
|
|
||||||
|
if channel.json_body.get("rooms", None) is None:
|
||||||
|
return None
|
||||||
|
|
||||||
# Return the read receipt
|
# Return the read receipt
|
||||||
ephemeral_events = channel.json_body["rooms"]["join"][self.room_id][
|
ephemeral_events = channel.json_body["rooms"]["join"][self.room_id][
|
||||||
"ephemeral"
|
"ephemeral"
|
||||||
|
@ -499,7 +562,10 @@ class UnreadMessagesTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
def default_config(self) -> JsonDict:
|
def default_config(self) -> JsonDict:
|
||||||
config = super().default_config()
|
config = super().default_config()
|
||||||
config["experimental_features"] = {"msc2654_enabled": True}
|
config["experimental_features"] = {
|
||||||
|
"msc2654_enabled": True,
|
||||||
|
"msc2285_enabled": True,
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
@ -564,7 +630,7 @@ class UnreadMessagesTestCase(unittest.HomeserverTestCase):
|
||||||
body = json.dumps({ReceiptTypes.READ: res["event_id"]}).encode("utf8")
|
body = json.dumps({ReceiptTypes.READ: res["event_id"]}).encode("utf8")
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"POST",
|
"POST",
|
||||||
"/rooms/%s/read_markers" % self.room_id,
|
f"/rooms/{self.room_id}/read_markers",
|
||||||
body,
|
body,
|
||||||
access_token=self.tok,
|
access_token=self.tok,
|
||||||
)
|
)
|
||||||
|
@ -578,11 +644,10 @@ class UnreadMessagesTestCase(unittest.HomeserverTestCase):
|
||||||
self._check_unread_count(1)
|
self._check_unread_count(1)
|
||||||
|
|
||||||
# Send a read receipt to tell the server we've read the latest event.
|
# Send a read receipt to tell the server we've read the latest event.
|
||||||
body = json.dumps({ReadReceiptEventFields.MSC2285_HIDDEN: True}).encode("utf8")
|
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"POST",
|
"POST",
|
||||||
"/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]),
|
f"/rooms/{self.room_id}/receipt/org.matrix.msc2285.read.private/{res['event_id']}",
|
||||||
body,
|
{},
|
||||||
access_token=self.tok,
|
access_token=self.tok,
|
||||||
)
|
)
|
||||||
self.assertEqual(channel.code, 200, channel.json_body)
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
@ -644,13 +709,73 @@ class UnreadMessagesTestCase(unittest.HomeserverTestCase):
|
||||||
self._check_unread_count(4)
|
self._check_unread_count(4)
|
||||||
|
|
||||||
# Check that tombstone events changes increase the unread counter.
|
# Check that tombstone events changes increase the unread counter.
|
||||||
self.helper.send_state(
|
res1 = self.helper.send_state(
|
||||||
self.room_id,
|
self.room_id,
|
||||||
EventTypes.Tombstone,
|
EventTypes.Tombstone,
|
||||||
{"replacement_room": "!someroom:test"},
|
{"replacement_room": "!someroom:test"},
|
||||||
tok=self.tok2,
|
tok=self.tok2,
|
||||||
)
|
)
|
||||||
self._check_unread_count(5)
|
self._check_unread_count(5)
|
||||||
|
res2 = self.helper.send(self.room_id, "hello", tok=self.tok2)
|
||||||
|
|
||||||
|
# Make sure both m.read and org.matrix.msc2285.read.private advance
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{self.room_id}/receipt/m.read/{res1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=self.tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
self._check_unread_count(1)
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{self.room_id}/receipt/org.matrix.msc2285.read.private/{res2['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=self.tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
self._check_unread_count(0)
|
||||||
|
|
||||||
|
# We test for both receipt types that influence notification counts
|
||||||
|
@parameterized.expand([ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE])
|
||||||
|
def test_read_receipts_only_go_down(self, receipt_type: ReceiptTypes) -> None:
|
||||||
|
# Join the new user
|
||||||
|
self.helper.join(room=self.room_id, user=self.user2, tok=self.tok2)
|
||||||
|
|
||||||
|
# Send messages
|
||||||
|
res1 = self.helper.send(self.room_id, "hello", tok=self.tok2)
|
||||||
|
res2 = self.helper.send(self.room_id, "hello", tok=self.tok2)
|
||||||
|
|
||||||
|
# Read last event
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{self.room_id}/receipt/{receipt_type}/{res2['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=self.tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
self._check_unread_count(0)
|
||||||
|
|
||||||
|
# Make sure neither m.read nor org.matrix.msc2285.read.private make the
|
||||||
|
# read receipt go up to an older event
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{self.room_id}/receipt/org.matrix.msc2285.read.private/{res1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=self.tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
self._check_unread_count(0)
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
f"/rooms/{self.room_id}/receipt/m.read/{res1['event_id']}",
|
||||||
|
{},
|
||||||
|
access_token=self.tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
self._check_unread_count(0)
|
||||||
|
|
||||||
def _check_unread_count(self, expected_count: int) -> None:
|
def _check_unread_count(self, expected_count: int) -> None:
|
||||||
"""Syncs and compares the unread count with the expected value."""
|
"""Syncs and compares the unread count with the expected value."""
|
||||||
|
@ -663,9 +788,11 @@ class UnreadMessagesTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
self.assertEqual(channel.code, 200, channel.json_body)
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
room_entry = channel.json_body["rooms"]["join"][self.room_id]
|
room_entry = (
|
||||||
|
channel.json_body.get("rooms", {}).get("join", {}).get(self.room_id, {})
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
room_entry["org.matrix.msc2654.unread_count"],
|
room_entry.get("org.matrix.msc2654.unread_count", 0),
|
||||||
expected_count,
|
expected_count,
|
||||||
room_entry,
|
room_entry,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue