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

Commit

Permalink
Prevent duplicate push notifications for room reads (#11835)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasdenk committed Feb 17, 2022
1 parent 73fc488 commit 4077177
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 66 deletions.
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,
)
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)

0 comments on commit 4077177

Please sign in to comment.