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

Prevent duplicate push notifications for room reads #11835

Merged
merged 9 commits into from
Feb 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/11835.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make a `POST` to `/rooms/<room_id>/receipt/m.read/<event_id>` only trigger a push notification if the count of unread messages is different to the one in the last successfully sent push.
7 changes: 6 additions & 1 deletion synapse/push/httppusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def __init__(self, hs: "HomeServer", pusher_config: PusherConfig):
self.data_minus_url = {}
self.data_minus_url.update(self.data)
del self.data_minus_url["url"]
self.badge_count_last_call: Optional[int] = None

def on_started(self, should_check_for_notifs: bool) -> None:
"""Called when this pusher has been started.
Expand Down Expand Up @@ -136,7 +137,9 @@ async def _update_badge(self) -> None:
self.user_id,
group_by_room=self._group_unread_count_by_room,
)
await self._send_badge(badge)
if self.badge_count_last_call is None or self.badge_count_last_call != badge:
self.badge_count_last_call = badge
await self._send_badge(badge)

def on_timer(self) -> None:
self._start_processing()
Expand Down Expand Up @@ -402,6 +405,8 @@ async def dispatch_push(
rejected = []
if "rejected" in resp:
rejected = resp["rejected"]
else:
self.badge_count_last_call = badge
return rejected

async def _send_badge(self, badge: int) -> None:
Expand Down
129 changes: 64 additions & 65 deletions tests/push/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,7 @@ def test_push_unread_count_group_by_room(self):
# Carry out our option-value specific test
#
# This push should still only contain an unread count of 1 (for 1 unread room)
self.assertEqual(
self.push_attempts[5][2]["notification"]["counts"]["unread"], 1
)
self._check_push_attempt(6, 1)

@override_config({"push": {"group_unread_count_by_room": False}})
def test_push_unread_count_message_count(self):
Expand All @@ -585,20 +583,19 @@ def test_push_unread_count_message_count(self):

# Carry out our option-value specific test
#
# We're counting every unread message, so there should now be 4 since the
# We're counting every unread message, so there should now be 3 since the
# last read receipt
self.assertEqual(
self.push_attempts[5][2]["notification"]["counts"]["unread"], 4
)
self._check_push_attempt(6, 3)

def _test_push_unread_count(self):
"""
Tests that the correct unread count appears in sent push notifications

Note that:
* Sending messages will cause push notifications to go out to relevant users
* Sending a read receipt will cause a "badge update" notification to go out to
the user that sent the receipt
* Sending a read receipt will cause the HTTP pusher to check whether the unread
count has changed since the last push notification. If so, a "badge update"
notification goes out to the user that sent the receipt
"""
# Register the user who gets notified
user_id = self.register_user("user", "pass")
Expand Down Expand Up @@ -642,81 +639,83 @@ def _test_push_unread_count(self):
# position in the room. We'll set the read position to this event in a moment
first_message_event_id = response["event_id"]

# Advance time a bit (so the pusher will register something has happened) and
# make the push succeed
self.push_attempts[0][0].callback({})
expected_push_attempts = 1
self._check_push_attempt(expected_push_attempts, 0)

self._send_read_request(access_token, first_message_event_id, room_id)

# Unread count has not changed. Therefore, ensure that read request does not
# trigger a push notification.
self.assertEqual(len(self.push_attempts), 1)

# Send another message
response2 = self.helper.send(
room_id, body="How's the weather today?", tok=other_access_token
)
second_message_event_id = response2["event_id"]

expected_push_attempts += 1

self._check_push_attempt(expected_push_attempts, 1)

self._send_read_request(access_token, second_message_event_id, room_id)
expected_push_attempts += 1

self._check_push_attempt(expected_push_attempts, 0)

# If we're grouping by room, sending more messages shouldn't increase the
# unread count, as they're all being sent in the same room. Otherwise, it
# should. Therefore, the last call to _check_push_attempt is done in the
# caller method.
self.helper.send(room_id, body="Hello?", tok=other_access_token)
expected_push_attempts += 1

self._advance_time_and_make_push_succeed(expected_push_attempts)

self.helper.send(room_id, body="Hello??", tok=other_access_token)
expected_push_attempts += 1

self._advance_time_and_make_push_succeed(expected_push_attempts)

self.helper.send(room_id, body="HELLO???", tok=other_access_token)

def _advance_time_and_make_push_succeed(self, expected_push_attempts):
self.pump()
self.push_attempts[expected_push_attempts - 1][0].callback({})

def _check_push_attempt(
self, expected_push_attempts: int, expected_unread_count_last_push: int
) -> None:
"""
Makes sure that the last expected push attempt succeeds and checks whether
it contains the expected unread count.
"""
self._advance_time_and_make_push_succeed(expected_push_attempts)
# Check our push made it
self.assertEqual(len(self.push_attempts), 1)
self.assertEqual(len(self.push_attempts), expected_push_attempts)
_, push_url, push_body = self.push_attempts[expected_push_attempts - 1]
self.assertEqual(
self.push_attempts[0][1], "http://example.com/_matrix/push/v1/notify"
push_url,
"http://example.com/_matrix/push/v1/notify",
)

# Check that the unread count for the room is 0
#
# The unread count is zero as the user has no read receipt in the room yet
self.assertEqual(
self.push_attempts[0][2]["notification"]["counts"]["unread"], 0
push_body["notification"]["counts"]["unread"],
expected_unread_count_last_push,
)

def _send_read_request(self, access_token, message_event_id, room_id):
# Now set the user's read receipt position to the first event
#
# This will actually trigger a new notification to be sent out so that
# even if the user does not receive another message, their unread
# count goes down
channel = self.make_request(
"POST",
"/rooms/%s/receipt/m.read/%s" % (room_id, first_message_event_id),
"/rooms/%s/receipt/m.read/%s" % (room_id, message_event_id),
{},
access_token=access_token,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a comment just above 'This will actually trigger a new notification to be sent out so that even if the user does not receive another message, their unread count goes down'...

It doesn't make much sense to me though because it was already at zero right before.

I would say 'I think it may be worth removing the comment', but this test is trying to test that pushes go out when you read something. As it stands, we're not testing that anymore.
Do you think you can adapt the test so that it will actually get the chance to send a push after a read receipt?

The test currently looks like:

  • send message → send count of 0 (because we haven't got a read receipt in the room yet)
  • add read receipt → send count of 0 (because we sent a read receipt) — your changes stop this from happening, so we don't actually end up testing that read receipts do anything
  • send message → send count of 1

I wonder if it should look like this:

  • send message → send count of 0 (because we haven't got a read receipt in the room yet)
  • add read receipt → make sure nothing new has been sent (that's still a valuable piece of test information) — do it with an assert on len(push_attempts)
  • send message → send count of 1
  • add read receipt (to the second message) → make sure we send a count of 0
  • send message → send count of 1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I will start working on that issue by the beginning of next week.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reivilibre: I still refactored the test a bit. If you do not like it, the commit before preserves
the old coding style.

self.assertEqual(channel.code, 200, channel.json_body)

# Advance time and make the push succeed
self.push_attempts[1][0].callback({})
self.pump()

# Unread count is still zero as we've read the only message in the room
self.assertEqual(len(self.push_attempts), 2)
self.assertEqual(
self.push_attempts[1][2]["notification"]["counts"]["unread"], 0
)

# Send another message
self.helper.send(
room_id, body="How's the weather today?", tok=other_access_token
)

# Advance time and make the push succeed
self.push_attempts[2][0].callback({})
self.pump()

# This push should contain an unread count of 1 as there's now been one
# message since our last read receipt
self.assertEqual(len(self.push_attempts), 3)
self.assertEqual(
self.push_attempts[2][2]["notification"]["counts"]["unread"], 1
)

# Since we're grouping by room, sending more messages shouldn't increase the
# unread count, as they're all being sent in the same room
self.helper.send(room_id, body="Hello?", tok=other_access_token)

# Advance time and make the push succeed
self.pump()
self.push_attempts[3][0].callback({})

self.helper.send(room_id, body="Hello??", tok=other_access_token)

# Advance time and make the push succeed
self.pump()
self.push_attempts[4][0].callback({})

self.helper.send(room_id, body="HELLO???", tok=other_access_token)

# Advance time and make the push succeed
self.pump()
self.push_attempts[5][0].callback({})

self.assertEqual(len(self.push_attempts), 6)