mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-09-06 23:32:03 +00:00
Mark events as read using threaded read receipts from MSC3771. (#13877)
Applies the proper logic for unthreaded and threaded receipts to either apply to all events in the room or only events in the same thread, respectively.
This commit is contained in:
parent
f0019f3f3b
commit
a7ba457b2b
1
changelog.d/13877.feature
Normal file
1
changelog.d/13877.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Experimental support for thread-specific receipts ([MSC3771](https://github.com/matrix-org/matrix-spec-proposals/pull/3771)).
|
@ -421,7 +421,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
txn: LoggingTransaction,
|
txn: LoggingTransaction,
|
||||||
room_id: str,
|
room_id: str,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
receipt_stream_ordering: int,
|
unthreaded_receipt_stream_ordering: int,
|
||||||
) -> RoomNotifCounts:
|
) -> RoomNotifCounts:
|
||||||
"""Get the number of unread messages for a user/room that have happened
|
"""Get the number of unread messages for a user/room that have happened
|
||||||
since the given stream ordering.
|
since the given stream ordering.
|
||||||
@ -430,9 +430,9 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
txn: The database transaction.
|
txn: The database transaction.
|
||||||
room_id: The room ID to get unread counts for.
|
room_id: The room ID to get unread counts for.
|
||||||
user_id: The user ID to get unread counts for.
|
user_id: The user ID to get unread counts for.
|
||||||
receipt_stream_ordering: The stream ordering of the user's latest
|
unthreaded_receipt_stream_ordering: The stream ordering of the user's latest
|
||||||
receipt in the room. If there are no receipts, the stream ordering
|
unthreaded receipt in the room. If there are no unthreaded receipts,
|
||||||
of the user's join event.
|
the stream ordering of the user's join event.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A RoomNotifCounts object containing the notification count, the
|
A RoomNotifCounts object containing the notification count, the
|
||||||
@ -448,71 +448,181 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
return main_counts
|
return main_counts
|
||||||
return thread_counts.setdefault(thread_id, NotifCounts())
|
return thread_counts.setdefault(thread_id, NotifCounts())
|
||||||
|
|
||||||
|
receipt_types_clause, receipts_args = make_in_list_sql_clause(
|
||||||
|
self.database_engine,
|
||||||
|
"receipt_type",
|
||||||
|
(ReceiptTypes.READ, ReceiptTypes.READ_PRIVATE),
|
||||||
|
)
|
||||||
|
|
||||||
# First we pull the counts from the summary table.
|
# First we pull the counts from the summary table.
|
||||||
#
|
#
|
||||||
# We check that `last_receipt_stream_ordering` matches the stream
|
# We check that `last_receipt_stream_ordering` matches the stream ordering of the
|
||||||
# ordering given. If it doesn't match then a new read receipt has arrived and
|
# latest receipt for the thread (which may be either the unthreaded read receipt
|
||||||
# we haven't yet updated the counts in `event_push_summary` to reflect
|
# or the threaded read receipt).
|
||||||
# that; in that case we simply ignore `event_push_summary` counts
|
|
||||||
# and do a manual count of all of the rows in the `event_push_actions` table
|
|
||||||
# for this user/room.
|
|
||||||
#
|
#
|
||||||
# If `last_receipt_stream_ordering` is null then that means it's up to
|
# If it doesn't match then a new read receipt has arrived and we haven't yet
|
||||||
# date (as the row was written by an older version of Synapse that
|
# updated the counts in `event_push_summary` to reflect that; in that case we
|
||||||
|
# simply ignore `event_push_summary` counts.
|
||||||
|
#
|
||||||
|
# We then do a manual count of all the rows in the `event_push_actions` table
|
||||||
|
# for any user/room/thread which did not have a valid summary found.
|
||||||
|
#
|
||||||
|
# If `last_receipt_stream_ordering` is null then that means it's up-to-date
|
||||||
|
# (as the row was written by an older version of Synapse that
|
||||||
# updated `event_push_summary` synchronously when persisting a new read
|
# updated `event_push_summary` synchronously when persisting a new read
|
||||||
# receipt).
|
# receipt).
|
||||||
txn.execute(
|
txn.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT stream_ordering, notif_count, COALESCE(unread_count, 0), thread_id
|
SELECT notif_count, COALESCE(unread_count, 0), thread_id
|
||||||
FROM event_push_summary
|
FROM event_push_summary
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT thread_id, MAX(stream_ordering) AS threaded_receipt_stream_ordering
|
||||||
|
FROM receipts_linearized
|
||||||
|
LEFT JOIN events USING (room_id, event_id)
|
||||||
|
WHERE
|
||||||
|
user_id = ?
|
||||||
|
AND room_id = ?
|
||||||
|
AND stream_ordering > ?
|
||||||
|
AND {receipt_types_clause}
|
||||||
|
GROUP BY thread_id
|
||||||
|
) AS receipts USING (thread_id)
|
||||||
WHERE room_id = ? AND user_id = ?
|
WHERE room_id = ? AND user_id = ?
|
||||||
AND (
|
AND (
|
||||||
(last_receipt_stream_ordering IS NULL AND stream_ordering > ?)
|
(last_receipt_stream_ordering IS NULL AND stream_ordering > COALESCE(threaded_receipt_stream_ordering, ?))
|
||||||
OR last_receipt_stream_ordering = ?
|
OR last_receipt_stream_ordering = COALESCE(threaded_receipt_stream_ordering, ?)
|
||||||
) AND (notif_count != 0 OR COALESCE(unread_count, 0) != 0)
|
) AND (notif_count != 0 OR COALESCE(unread_count, 0) != 0)
|
||||||
""",
|
""",
|
||||||
(room_id, user_id, receipt_stream_ordering, receipt_stream_ordering),
|
(
|
||||||
|
user_id,
|
||||||
|
room_id,
|
||||||
|
unthreaded_receipt_stream_ordering,
|
||||||
|
*receipts_args,
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
|
unthreaded_receipt_stream_ordering,
|
||||||
|
unthreaded_receipt_stream_ordering,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
max_summary_stream_ordering = 0
|
summarised_threads = set()
|
||||||
for summary_stream_ordering, notif_count, unread_count, thread_id in txn:
|
for notif_count, unread_count, thread_id in txn:
|
||||||
|
summarised_threads.add(thread_id)
|
||||||
counts = _get_thread(thread_id)
|
counts = _get_thread(thread_id)
|
||||||
counts.notify_count += notif_count
|
counts.notify_count += notif_count
|
||||||
counts.unread_count += unread_count
|
counts.unread_count += unread_count
|
||||||
|
|
||||||
# Summaries will only be used if they have not been invalidated by
|
|
||||||
# a recent receipt; track the latest stream ordering or a valid summary.
|
|
||||||
#
|
|
||||||
# Note that since there's only one read receipt in the room per user,
|
|
||||||
# valid summaries are contiguous.
|
|
||||||
max_summary_stream_ordering = max(
|
|
||||||
summary_stream_ordering, max_summary_stream_ordering
|
|
||||||
)
|
|
||||||
|
|
||||||
# Next we need to count highlights, which aren't summarised
|
# Next we need to count highlights, which aren't summarised
|
||||||
sql = """
|
sql = f"""
|
||||||
SELECT COUNT(*), thread_id FROM event_push_actions
|
SELECT COUNT(*), thread_id FROM event_push_actions
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT thread_id, MAX(stream_ordering) AS threaded_receipt_stream_ordering
|
||||||
|
FROM receipts_linearized
|
||||||
|
LEFT JOIN events USING (room_id, event_id)
|
||||||
|
WHERE
|
||||||
|
user_id = ?
|
||||||
|
AND room_id = ?
|
||||||
|
AND stream_ordering > ?
|
||||||
|
AND {receipt_types_clause}
|
||||||
|
GROUP BY thread_id
|
||||||
|
) AS receipts USING (thread_id)
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND room_id = ?
|
AND room_id = ?
|
||||||
AND stream_ordering > ?
|
AND stream_ordering > COALESCE(threaded_receipt_stream_ordering, ?)
|
||||||
AND highlight = 1
|
AND highlight = 1
|
||||||
GROUP BY thread_id
|
GROUP BY thread_id
|
||||||
"""
|
"""
|
||||||
txn.execute(sql, (user_id, room_id, receipt_stream_ordering))
|
txn.execute(
|
||||||
|
sql,
|
||||||
|
(
|
||||||
|
user_id,
|
||||||
|
room_id,
|
||||||
|
unthreaded_receipt_stream_ordering,
|
||||||
|
*receipts_args,
|
||||||
|
user_id,
|
||||||
|
room_id,
|
||||||
|
unthreaded_receipt_stream_ordering,
|
||||||
|
),
|
||||||
|
)
|
||||||
for highlight_count, thread_id in txn:
|
for highlight_count, thread_id in txn:
|
||||||
_get_thread(thread_id).highlight_count += highlight_count
|
_get_thread(thread_id).highlight_count += highlight_count
|
||||||
|
|
||||||
|
# For threads which were summarised we need to count actions since the last
|
||||||
|
# rotation.
|
||||||
|
thread_id_clause, thread_id_args = make_in_list_sql_clause(
|
||||||
|
self.database_engine, "thread_id", summarised_threads
|
||||||
|
)
|
||||||
|
|
||||||
|
# The (inclusive) event stream ordering that was previously summarised.
|
||||||
|
rotated_upto_stream_ordering = self.db_pool.simple_select_one_onecol_txn(
|
||||||
|
txn,
|
||||||
|
table="event_push_summary_stream_ordering",
|
||||||
|
keyvalues={},
|
||||||
|
retcol="stream_ordering",
|
||||||
|
)
|
||||||
|
|
||||||
|
unread_counts = self._get_notif_unread_count_for_user_room(
|
||||||
|
txn, room_id, user_id, rotated_upto_stream_ordering
|
||||||
|
)
|
||||||
|
for notif_count, unread_count, thread_id in unread_counts:
|
||||||
|
if thread_id not in summarised_threads:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if thread_id == MAIN_TIMELINE:
|
||||||
|
counts.notify_count += notif_count
|
||||||
|
counts.unread_count += unread_count
|
||||||
|
elif thread_id in thread_counts:
|
||||||
|
thread_counts[thread_id].notify_count += notif_count
|
||||||
|
thread_counts[thread_id].unread_count += unread_count
|
||||||
|
else:
|
||||||
|
# Previous thread summaries of 0 are discarded above.
|
||||||
|
#
|
||||||
|
# TODO If empty summaries are deleted this can be removed.
|
||||||
|
thread_counts[thread_id] = NotifCounts(
|
||||||
|
notify_count=notif_count,
|
||||||
|
unread_count=unread_count,
|
||||||
|
highlight_count=0,
|
||||||
|
)
|
||||||
|
|
||||||
# Finally we need to count push actions that aren't included in the
|
# Finally we need to count push actions that aren't included in the
|
||||||
# summary returned above. This might be due to recent events that haven't
|
# summary returned above. This might be due to recent events that haven't
|
||||||
# been summarised yet or the summary is out of date due to a recent read
|
# been summarised yet or the summary is out of date due to a recent read
|
||||||
# receipt.
|
# receipt.
|
||||||
start_unread_stream_ordering = max(
|
sql = f"""
|
||||||
receipt_stream_ordering, max_summary_stream_ordering
|
SELECT
|
||||||
|
COUNT(CASE WHEN notif = 1 THEN 1 END),
|
||||||
|
COUNT(CASE WHEN unread = 1 THEN 1 END),
|
||||||
|
thread_id
|
||||||
|
FROM event_push_actions
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT thread_id, MAX(stream_ordering) AS threaded_receipt_stream_ordering
|
||||||
|
FROM receipts_linearized
|
||||||
|
LEFT JOIN events USING (room_id, event_id)
|
||||||
|
WHERE
|
||||||
|
user_id = ?
|
||||||
|
AND room_id = ?
|
||||||
|
AND stream_ordering > ?
|
||||||
|
AND {receipt_types_clause}
|
||||||
|
GROUP BY thread_id
|
||||||
|
) AS receipts USING (thread_id)
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND room_id = ?
|
||||||
|
AND stream_ordering > COALESCE(threaded_receipt_stream_ordering, ?)
|
||||||
|
AND NOT {thread_id_clause}
|
||||||
|
GROUP BY thread_id
|
||||||
|
"""
|
||||||
|
txn.execute(
|
||||||
|
sql,
|
||||||
|
(
|
||||||
|
user_id,
|
||||||
|
room_id,
|
||||||
|
unthreaded_receipt_stream_ordering,
|
||||||
|
*receipts_args,
|
||||||
|
user_id,
|
||||||
|
room_id,
|
||||||
|
unthreaded_receipt_stream_ordering,
|
||||||
|
*thread_id_args,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
unread_counts = self._get_notif_unread_count_for_user_room(
|
for notif_count, unread_count, thread_id in txn:
|
||||||
txn, room_id, user_id, start_unread_stream_ordering
|
|
||||||
)
|
|
||||||
|
|
||||||
for notif_count, unread_count, thread_id in unread_counts:
|
|
||||||
counts = _get_thread(thread_id)
|
counts = _get_thread(thread_id)
|
||||||
counts.notify_count += notif_count
|
counts.notify_count += notif_count
|
||||||
counts.unread_count += unread_count
|
counts.unread_count += unread_count
|
||||||
@ -526,6 +636,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
user_id: str,
|
user_id: str,
|
||||||
stream_ordering: int,
|
stream_ordering: int,
|
||||||
max_stream_ordering: Optional[int] = None,
|
max_stream_ordering: Optional[int] = None,
|
||||||
|
thread_id: Optional[str] = None,
|
||||||
) -> List[Tuple[int, int, str]]:
|
) -> List[Tuple[int, int, str]]:
|
||||||
"""Returns the notify and unread counts from `event_push_actions` for
|
"""Returns the notify and unread counts from `event_push_actions` for
|
||||||
the given user/room in the given range.
|
the given user/room in the given range.
|
||||||
@ -540,6 +651,11 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
stream_ordering: The (exclusive) minimum stream ordering to consider.
|
stream_ordering: The (exclusive) minimum stream ordering to consider.
|
||||||
max_stream_ordering: The (inclusive) maximum stream ordering to consider.
|
max_stream_ordering: The (inclusive) maximum stream ordering to consider.
|
||||||
If this is not given, then no maximum is applied.
|
If this is not given, then no maximum is applied.
|
||||||
|
thread_id: The thread ID to fetch unread counts for. If this is not provided
|
||||||
|
then the results for *all* threads is returned.
|
||||||
|
|
||||||
|
Note that if this is provided the resulting list will only have 0 or
|
||||||
|
1 tuples in it.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A tuple of the notif count and unread count in the given range for
|
A tuple of the notif count and unread count in the given range for
|
||||||
@ -551,10 +667,10 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
if not self._events_stream_cache.has_entity_changed(room_id, stream_ordering):
|
if not self._events_stream_cache.has_entity_changed(room_id, stream_ordering):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
clause = ""
|
stream_ordering_clause = ""
|
||||||
args = [user_id, room_id, stream_ordering]
|
args = [user_id, room_id, stream_ordering]
|
||||||
if max_stream_ordering is not None:
|
if max_stream_ordering is not None:
|
||||||
clause = "AND ea.stream_ordering <= ?"
|
stream_ordering_clause = "AND ea.stream_ordering <= ?"
|
||||||
args.append(max_stream_ordering)
|
args.append(max_stream_ordering)
|
||||||
|
|
||||||
# If the max stream ordering is less than the min stream ordering,
|
# If the max stream ordering is less than the min stream ordering,
|
||||||
@ -562,6 +678,12 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
if max_stream_ordering <= stream_ordering:
|
if max_stream_ordering <= stream_ordering:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Either limit the results to a specific thread or fetch all threads.
|
||||||
|
thread_id_clause = ""
|
||||||
|
if thread_id is not None:
|
||||||
|
thread_id_clause = "AND thread_id = ?"
|
||||||
|
args.append(thread_id)
|
||||||
|
|
||||||
sql = f"""
|
sql = f"""
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(CASE WHEN notif = 1 THEN 1 END),
|
COUNT(CASE WHEN notif = 1 THEN 1 END),
|
||||||
@ -571,7 +693,8 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND room_id = ?
|
AND room_id = ?
|
||||||
AND ea.stream_ordering > ?
|
AND ea.stream_ordering > ?
|
||||||
{clause}
|
{stream_ordering_clause}
|
||||||
|
{thread_id_clause}
|
||||||
GROUP BY thread_id
|
GROUP BY thread_id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -1086,7 +1209,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
)
|
)
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
SELECT r.stream_id, r.room_id, r.user_id, e.stream_ordering
|
SELECT r.stream_id, r.room_id, r.user_id, r.thread_id, e.stream_ordering
|
||||||
FROM receipts_linearized AS r
|
FROM receipts_linearized AS r
|
||||||
INNER JOIN events AS e USING (event_id)
|
INNER JOIN events AS e USING (event_id)
|
||||||
WHERE ? < r.stream_id AND r.stream_id <= ? AND user_id LIKE ?
|
WHERE ? < r.stream_id AND r.stream_id <= ? AND user_id LIKE ?
|
||||||
@ -1107,44 +1230,68 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
limit,
|
limit,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
rows = cast(List[Tuple[int, str, str, int]], txn.fetchall())
|
rows = cast(List[Tuple[int, str, str, Optional[str], int]], txn.fetchall())
|
||||||
|
|
||||||
# For each new read receipt we delete push actions from before it and
|
# For each new read receipt we delete push actions from before it and
|
||||||
# recalculate the summary.
|
# recalculate the summary.
|
||||||
for _, room_id, user_id, stream_ordering in rows:
|
#
|
||||||
|
# Care must be taken of whether it is a threaded or unthreaded receipt.
|
||||||
|
for _, room_id, user_id, thread_id, stream_ordering in rows:
|
||||||
# Only handle our own read receipts.
|
# Only handle our own read receipts.
|
||||||
if not self.hs.is_mine_id(user_id):
|
if not self.hs.is_mine_id(user_id):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
thread_clause = ""
|
||||||
|
thread_args: Tuple = ()
|
||||||
|
if thread_id is not None:
|
||||||
|
thread_clause = "AND thread_id = ?"
|
||||||
|
thread_args = (thread_id,)
|
||||||
|
|
||||||
|
# For each new read receipt we delete push actions from before it and
|
||||||
|
# recalculate the summary.
|
||||||
txn.execute(
|
txn.execute(
|
||||||
"""
|
f"""
|
||||||
DELETE FROM event_push_actions
|
DELETE FROM event_push_actions
|
||||||
WHERE room_id = ?
|
WHERE room_id = ?
|
||||||
AND user_id = ?
|
AND user_id = ?
|
||||||
AND stream_ordering <= ?
|
AND stream_ordering <= ?
|
||||||
AND highlight = 0
|
AND highlight = 0
|
||||||
|
{thread_clause}
|
||||||
""",
|
""",
|
||||||
(room_id, user_id, stream_ordering),
|
(room_id, user_id, stream_ordering, *thread_args),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fetch the notification counts between the stream ordering of the
|
# Fetch the notification counts between the stream ordering of the
|
||||||
# latest receipt and what was previously summarised.
|
# latest receipt and what was previously summarised.
|
||||||
unread_counts = self._get_notif_unread_count_for_user_room(
|
unread_counts = self._get_notif_unread_count_for_user_room(
|
||||||
txn, room_id, user_id, stream_ordering, old_rotate_stream_ordering
|
txn,
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
|
stream_ordering,
|
||||||
|
old_rotate_stream_ordering,
|
||||||
|
thread_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
# First mark the summary for all threads in the room as cleared.
|
# For an unthreaded receipt, mark the summary for all threads in the room
|
||||||
self.db_pool.simple_update_txn(
|
# as cleared.
|
||||||
txn,
|
if thread_id is None:
|
||||||
table="event_push_summary",
|
self.db_pool.simple_update_txn(
|
||||||
keyvalues={"user_id": user_id, "room_id": room_id},
|
txn,
|
||||||
updatevalues={
|
table="event_push_summary",
|
||||||
"notif_count": 0,
|
keyvalues={"user_id": user_id, "room_id": room_id},
|
||||||
"unread_count": 0,
|
updatevalues={
|
||||||
"stream_ordering": old_rotate_stream_ordering,
|
"notif_count": 0,
|
||||||
"last_receipt_stream_ordering": stream_ordering,
|
"unread_count": 0,
|
||||||
},
|
"stream_ordering": old_rotate_stream_ordering,
|
||||||
)
|
"last_receipt_stream_ordering": stream_ordering,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# For a threaded receipt, we *always* want to update that receipt,
|
||||||
|
# event if there are no new notifications in that thread. This ensures
|
||||||
|
# the stream_ordering & last_receipt_stream_ordering are updated.
|
||||||
|
elif not unread_counts:
|
||||||
|
unread_counts = [(0, 0, thread_id)]
|
||||||
|
|
||||||
# Then any updated threads get their notification count and unread
|
# Then any updated threads get their notification count and unread
|
||||||
# count updated.
|
# count updated.
|
||||||
@ -1153,8 +1300,16 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
|
|||||||
table="event_push_summary",
|
table="event_push_summary",
|
||||||
key_names=("room_id", "user_id", "thread_id"),
|
key_names=("room_id", "user_id", "thread_id"),
|
||||||
key_values=[(room_id, user_id, row[2]) for row in unread_counts],
|
key_values=[(room_id, user_id, row[2]) for row in unread_counts],
|
||||||
value_names=("notif_count", "unread_count"),
|
value_names=(
|
||||||
value_values=[(row[0], row[1]) for row in unread_counts],
|
"notif_count",
|
||||||
|
"unread_count",
|
||||||
|
"stream_ordering",
|
||||||
|
"last_receipt_stream_ordering",
|
||||||
|
),
|
||||||
|
value_values=[
|
||||||
|
(row[0], row[1], old_rotate_stream_ordering, stream_ordering)
|
||||||
|
for row in unread_counts
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# We always update `event_push_summary_last_receipt_stream_id` to
|
# We always update `event_push_summary_last_receipt_stream_id` to
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
/* Copyright 2022 The Matrix.org Foundation C.I.C
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- Drop constraint on (room_id, receipt_type, user_id).
|
||||||
|
|
||||||
|
-- Rebuild the unique constraint with the thread_id.
|
||||||
|
ALTER TABLE receipts_linearized
|
||||||
|
DROP CONSTRAINT receipts_linearized_uniqueness;
|
||||||
|
|
||||||
|
ALTER TABLE receipts_graph
|
||||||
|
DROP CONSTRAINT receipts_graph_uniqueness;
|
@ -0,0 +1,76 @@
|
|||||||
|
/* Copyright 2022 The Matrix.org Foundation C.I.C
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- Drop constraint on (room_id, receipt_type, user_id).
|
||||||
|
--
|
||||||
|
-- SQLite doesn't support modifying constraints to an existing table, so it must
|
||||||
|
-- be recreated.
|
||||||
|
|
||||||
|
-- Create the new tables.
|
||||||
|
CREATE TABLE receipts_linearized_new (
|
||||||
|
stream_id BIGINT NOT NULL,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
receipt_type TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
thread_id TEXT,
|
||||||
|
event_stream_ordering BIGINT,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
CONSTRAINT receipts_linearized_uniqueness_thread UNIQUE (room_id, receipt_type, user_id, thread_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE receipts_graph_new (
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
receipt_type TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
event_ids TEXT NOT NULL,
|
||||||
|
thread_id TEXT,
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
CONSTRAINT receipts_graph_uniqueness_thread UNIQUE (room_id, receipt_type, user_id, thread_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Drop the old indexes.
|
||||||
|
DROP INDEX IF EXISTS receipts_linearized_id;
|
||||||
|
DROP INDEX IF EXISTS receipts_linearized_room_stream;
|
||||||
|
DROP INDEX IF EXISTS receipts_linearized_user;
|
||||||
|
|
||||||
|
-- Copy the data.
|
||||||
|
INSERT INTO receipts_linearized_new (stream_id, room_id, receipt_type, user_id, event_id, data)
|
||||||
|
SELECT stream_id, room_id, receipt_type, user_id, event_id, data
|
||||||
|
FROM receipts_linearized;
|
||||||
|
INSERT INTO receipts_graph_new (room_id, receipt_type, user_id, event_ids, data)
|
||||||
|
SELECT room_id, receipt_type, user_id, event_ids, data
|
||||||
|
FROM receipts_graph;
|
||||||
|
|
||||||
|
-- Drop the old tables.
|
||||||
|
DROP TABLE receipts_linearized;
|
||||||
|
DROP TABLE receipts_graph;
|
||||||
|
|
||||||
|
-- Rename the tables.
|
||||||
|
ALTER TABLE receipts_linearized_new RENAME TO receipts_linearized;
|
||||||
|
ALTER TABLE receipts_graph_new RENAME TO receipts_graph;
|
||||||
|
|
||||||
|
-- Create the indices.
|
||||||
|
CREATE INDEX receipts_linearized_id ON receipts_linearized( stream_id );
|
||||||
|
CREATE INDEX receipts_linearized_room_stream ON receipts_linearized( room_id, stream_id );
|
||||||
|
CREATE INDEX receipts_linearized_user ON receipts_linearized( user_id );
|
||||||
|
|
||||||
|
-- Re-run background updates from 72/08thread_receipts.sql.
|
||||||
|
INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
|
||||||
|
(7308, 'receipts_linearized_unique_index', '{}')
|
||||||
|
ON CONFLICT (update_name) DO NOTHING;
|
||||||
|
INSERT INTO background_updates (ordering, update_name, progress_json) VALUES
|
||||||
|
(7308, 'receipts_graph_unique_index', '{}')
|
||||||
|
ON CONFLICT (update_name) DO NOTHING;
|
@ -16,6 +16,7 @@ from typing import Optional, Tuple
|
|||||||
|
|
||||||
from twisted.test.proto_helpers import MemoryReactor
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
from synapse.api.constants import MAIN_TIMELINE
|
||||||
from synapse.rest import admin
|
from synapse.rest import admin
|
||||||
from synapse.rest.client import login, room
|
from synapse.rest.client import login, room
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
@ -250,6 +251,187 @@ class EventPushActionsStoreTestCase(HomeserverTestCase):
|
|||||||
|
|
||||||
last_event_id: str
|
last_event_id: str
|
||||||
|
|
||||||
|
def _assert_counts(
|
||||||
|
noitf_count: int,
|
||||||
|
highlight_count: int,
|
||||||
|
thread_notif_count: int,
|
||||||
|
thread_highlight_count: int,
|
||||||
|
) -> None:
|
||||||
|
counts = self.get_success(
|
||||||
|
self.store.db_pool.runInteraction(
|
||||||
|
"get-unread-counts",
|
||||||
|
self.store._get_unread_counts_by_receipt_txn,
|
||||||
|
room_id,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
counts.main_timeline,
|
||||||
|
NotifCounts(
|
||||||
|
notify_count=noitf_count,
|
||||||
|
unread_count=0,
|
||||||
|
highlight_count=highlight_count,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if thread_notif_count or thread_highlight_count:
|
||||||
|
self.assertEqual(
|
||||||
|
counts.threads,
|
||||||
|
{
|
||||||
|
thread_id: NotifCounts(
|
||||||
|
notify_count=thread_notif_count,
|
||||||
|
unread_count=0,
|
||||||
|
highlight_count=thread_highlight_count,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertEqual(counts.threads, {})
|
||||||
|
|
||||||
|
def _create_event(
|
||||||
|
highlight: bool = False, thread_id: Optional[str] = None
|
||||||
|
) -> str:
|
||||||
|
content: JsonDict = {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": user_id if highlight else "msg",
|
||||||
|
}
|
||||||
|
if thread_id:
|
||||||
|
content["m.relates_to"] = {
|
||||||
|
"rel_type": "m.thread",
|
||||||
|
"event_id": thread_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.helper.send_event(
|
||||||
|
room_id,
|
||||||
|
type="m.room.message",
|
||||||
|
content=content,
|
||||||
|
tok=other_token,
|
||||||
|
)
|
||||||
|
nonlocal last_event_id
|
||||||
|
last_event_id = result["event_id"]
|
||||||
|
return last_event_id
|
||||||
|
|
||||||
|
def _rotate() -> None:
|
||||||
|
self.get_success(self.store._rotate_notifs())
|
||||||
|
|
||||||
|
def _mark_read(event_id: str, thread_id: str = MAIN_TIMELINE) -> None:
|
||||||
|
self.get_success(
|
||||||
|
self.store.insert_receipt(
|
||||||
|
room_id,
|
||||||
|
"m.read",
|
||||||
|
user_id=user_id,
|
||||||
|
event_ids=[event_id],
|
||||||
|
thread_id=thread_id,
|
||||||
|
data={},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_assert_counts(0, 0, 0, 0)
|
||||||
|
thread_id = _create_event()
|
||||||
|
_assert_counts(1, 0, 0, 0)
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(1, 0, 0, 0)
|
||||||
|
|
||||||
|
_create_event(thread_id=thread_id)
|
||||||
|
_assert_counts(1, 0, 1, 0)
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(1, 0, 1, 0)
|
||||||
|
|
||||||
|
_create_event()
|
||||||
|
_assert_counts(2, 0, 1, 0)
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(2, 0, 1, 0)
|
||||||
|
|
||||||
|
event_id = _create_event(thread_id=thread_id)
|
||||||
|
_assert_counts(2, 0, 2, 0)
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(2, 0, 2, 0)
|
||||||
|
|
||||||
|
_create_event()
|
||||||
|
_create_event(thread_id=thread_id)
|
||||||
|
_mark_read(event_id)
|
||||||
|
_assert_counts(1, 0, 3, 0)
|
||||||
|
_mark_read(event_id, thread_id)
|
||||||
|
_assert_counts(1, 0, 1, 0)
|
||||||
|
|
||||||
|
_mark_read(last_event_id)
|
||||||
|
_mark_read(last_event_id, thread_id)
|
||||||
|
_assert_counts(0, 0, 0, 0)
|
||||||
|
|
||||||
|
_create_event()
|
||||||
|
_create_event(thread_id=thread_id)
|
||||||
|
_assert_counts(1, 0, 1, 0)
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(1, 0, 1, 0)
|
||||||
|
|
||||||
|
# Delete old event push actions, this should not affect the (summarised) count.
|
||||||
|
self.get_success(self.store._remove_old_push_actions_that_have_rotated())
|
||||||
|
_assert_counts(1, 0, 1, 0)
|
||||||
|
|
||||||
|
_mark_read(last_event_id)
|
||||||
|
_mark_read(last_event_id, thread_id)
|
||||||
|
_assert_counts(0, 0, 0, 0)
|
||||||
|
|
||||||
|
_create_event(True)
|
||||||
|
_assert_counts(1, 1, 0, 0)
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(1, 1, 0, 0)
|
||||||
|
|
||||||
|
event_id = _create_event(True, thread_id)
|
||||||
|
_assert_counts(1, 1, 1, 1)
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(1, 1, 1, 1)
|
||||||
|
|
||||||
|
# Check that adding another notification and rotating after highlight
|
||||||
|
# works.
|
||||||
|
_create_event()
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(2, 1, 1, 1)
|
||||||
|
|
||||||
|
_create_event(thread_id=thread_id)
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(2, 1, 2, 1)
|
||||||
|
|
||||||
|
# Check that sending read receipts at different points results in the
|
||||||
|
# right counts.
|
||||||
|
_mark_read(event_id)
|
||||||
|
_assert_counts(1, 0, 2, 1)
|
||||||
|
_mark_read(event_id, thread_id)
|
||||||
|
_assert_counts(1, 0, 1, 0)
|
||||||
|
_mark_read(last_event_id)
|
||||||
|
_assert_counts(0, 0, 1, 0)
|
||||||
|
_mark_read(last_event_id, thread_id)
|
||||||
|
_assert_counts(0, 0, 0, 0)
|
||||||
|
|
||||||
|
_create_event(True)
|
||||||
|
_create_event(True, thread_id)
|
||||||
|
_assert_counts(1, 1, 1, 1)
|
||||||
|
_mark_read(last_event_id)
|
||||||
|
_mark_read(last_event_id, thread_id)
|
||||||
|
_assert_counts(0, 0, 0, 0)
|
||||||
|
_rotate()
|
||||||
|
_assert_counts(0, 0, 0, 0)
|
||||||
|
|
||||||
|
def test_count_aggregation_mixed(self) -> None:
|
||||||
|
"""
|
||||||
|
This is essentially the same test as test_count_aggregation_threads, but
|
||||||
|
sends both unthreaded and threaded receipts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a user to receive notifications and send receipts.
|
||||||
|
user_id = self.register_user("user1235", "pass")
|
||||||
|
token = self.login("user1235", "pass")
|
||||||
|
|
||||||
|
# And another users to send events.
|
||||||
|
other_id = self.register_user("other", "pass")
|
||||||
|
other_token = self.login("other", "pass")
|
||||||
|
|
||||||
|
# Create a room and put both users in it.
|
||||||
|
room_id = self.helper.create_room_as(user_id, tok=token)
|
||||||
|
self.helper.join(room_id, other_id, tok=other_token)
|
||||||
|
thread_id: str
|
||||||
|
|
||||||
|
last_event_id: str
|
||||||
|
|
||||||
def _assert_counts(
|
def _assert_counts(
|
||||||
noitf_count: int,
|
noitf_count: int,
|
||||||
highlight_count: int,
|
highlight_count: int,
|
||||||
@ -350,7 +532,8 @@ class EventPushActionsStoreTestCase(HomeserverTestCase):
|
|||||||
_mark_read(event_id)
|
_mark_read(event_id)
|
||||||
_assert_counts(1, 0, 1, 0)
|
_assert_counts(1, 0, 1, 0)
|
||||||
|
|
||||||
_mark_read(last_event_id)
|
_mark_read(last_event_id, MAIN_TIMELINE)
|
||||||
|
_mark_read(last_event_id, thread_id)
|
||||||
_assert_counts(0, 0, 0, 0)
|
_assert_counts(0, 0, 0, 0)
|
||||||
|
|
||||||
_create_event()
|
_create_event()
|
||||||
@ -390,7 +573,11 @@ class EventPushActionsStoreTestCase(HomeserverTestCase):
|
|||||||
# right counts.
|
# right counts.
|
||||||
_mark_read(event_id)
|
_mark_read(event_id)
|
||||||
_assert_counts(1, 0, 1, 0)
|
_assert_counts(1, 0, 1, 0)
|
||||||
_mark_read(last_event_id)
|
_mark_read(event_id, MAIN_TIMELINE)
|
||||||
|
_assert_counts(1, 0, 1, 0)
|
||||||
|
_mark_read(last_event_id, MAIN_TIMELINE)
|
||||||
|
_assert_counts(0, 0, 1, 0)
|
||||||
|
_mark_read(last_event_id, thread_id)
|
||||||
_assert_counts(0, 0, 0, 0)
|
_assert_counts(0, 0, 0, 0)
|
||||||
|
|
||||||
_create_event(True)
|
_create_event(True)
|
||||||
|
Loading…
Reference in New Issue
Block a user