Refactor email summary generation. (#9260)

* Fixes a case where no summary text was returned.
* The use of messages_from_person vs. messages_from_person_and_others
  was tweaked to depend on whether there was 1 sender or multiple senders,
  not based on if there was 1 room or multiple rooms.
This commit is contained in:
Patrick Cloke 2021-02-01 13:09:39 -05:00 committed by GitHub
parent 18ab35284a
commit 5d38a3c97f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 202 additions and 120 deletions

1
changelog.d/9260.misc Normal file
View File

@ -0,0 +1 @@
Refactor the generation of summary text for email notifications.

View File

@ -267,8 +267,20 @@ class Mailer:
fallback_to_members=True, fallback_to_members=True,
) )
if len(notifs_by_room) == 1:
# Only one room has new stuff
room_id = list(notifs_by_room.keys())[0]
summary_text = await self.make_summary_text_single_room(
room_id,
notifs_by_room[room_id],
state_by_room[room_id],
notif_events,
user_id,
)
else:
summary_text = await self.make_summary_text( summary_text = await self.make_summary_text(
notifs_by_room, state_by_room, notif_events, user_id, reason notifs_by_room, state_by_room, notif_events, reason
) )
template_vars = { template_vars = {
@ -492,28 +504,37 @@ class Mailer:
if "url" in event.content: if "url" in event.content:
messagevars["image_url"] = event.content["url"] messagevars["image_url"] = event.content["url"]
async def make_summary_text( async def make_summary_text_single_room(
self, self,
notifs_by_room: Dict[str, List[Dict[str, Any]]], room_id: str,
room_state_ids: Dict[str, StateMap[str]], notifs: List[Dict[str, Any]],
room_state_ids: StateMap[str],
notif_events: Dict[str, EventBase], notif_events: Dict[str, EventBase],
user_id: str, user_id: str,
reason: Dict[str, Any], ) -> str:
): """
if len(notifs_by_room) == 1: Make a summary text for the email when only a single room has notifications.
# Only one room has new stuff
room_id = list(notifs_by_room.keys())[0]
Args:
room_id: The ID of the room.
notifs: The notifications for this room.
room_state_ids: The state map for the room.
notif_events: A map of event ID -> notification event.
user_id: The user receiving the notification.
Returns:
The summary text.
"""
# If the room has some kind of name, use it, but we don't # If the room has some kind of name, use it, but we don't
# want the generated-from-names one here otherwise we'll # want the generated-from-names one here otherwise we'll
# end up with, "new message from Bob in the Bob room" # end up with, "new message from Bob in the Bob room"
room_name = await calculate_room_name( room_name = await calculate_room_name(
self.store, room_state_ids[room_id], user_id, fallback_to_members=False self.store, room_state_ids, user_id, fallback_to_members=False
) )
# See if one of the notifs is an invite event for the user # See if one of the notifs is an invite event for the user
invite_event = None invite_event = None
for n in notifs_by_room[room_id]: for n in notifs:
ev = notif_events[n["event_id"]] ev = notif_events[n["event_id"]]
if ev.type == EventTypes.Member and ev.state_key == user_id: if ev.type == EventTypes.Member and ev.state_key == user_id:
if ev.content.get("membership") == Membership.INVITE: if ev.content.get("membership") == Membership.INVITE:
@ -521,7 +542,7 @@ class Mailer:
break break
if invite_event: if invite_event:
inviter_member_event_id = room_state_ids[room_id].get( inviter_member_event_id = room_state_ids.get(
("m.room.member", invite_event.sender) ("m.room.member", invite_event.sender)
) )
inviter_name = invite_event.sender inviter_name = invite_event.sender
@ -537,21 +558,19 @@ class Mailer:
"person": inviter_name, "person": inviter_name,
"app": self.app_name, "app": self.app_name,
} }
else:
return self.email_subjects.invite_from_person_to_room % { return self.email_subjects.invite_from_person_to_room % {
"person": inviter_name, "person": inviter_name,
"room": room_name, "room": room_name,
"app": self.app_name, "app": self.app_name,
} }
sender_name = None if len(notifs) == 1:
if len(notifs_by_room[room_id]) == 1:
# There is just the one notification, so give some detail # There is just the one notification, so give some detail
event = notif_events[notifs_by_room[room_id][0]["event_id"]] sender_name = None
if ("m.room.member", event.sender) in room_state_ids[room_id]: event = notif_events[notifs[0]["event_id"]]
state_event_id = room_state_ids[room_id][ if ("m.room.member", event.sender) in room_state_ids:
("m.room.member", event.sender) state_event_id = room_state_ids[("m.room.member", event.sender)]
]
state_event = await self.store.get_event(state_event_id) state_event = await self.store.get_event(state_event_id)
sender_name = name_from_member_event(state_event) sender_name = name_from_member_event(state_event)
@ -566,6 +585,12 @@ class Mailer:
"person": sender_name, "person": sender_name,
"app": self.app_name, "app": self.app_name,
} }
# The sender is unknown, just use the room name (or ID).
return self.email_subjects.messages_in_room % {
"room": room_name or room_id,
"app": self.app_name,
}
else: else:
# There's more than one notification for this room, so just # There's more than one notification for this room, so just
# say there are several # say there are several
@ -574,57 +599,83 @@ class Mailer:
"room": room_name, "room": room_name,
"app": self.app_name, "app": self.app_name,
} }
else:
# If the room doesn't have a name, say who the messages return await self.make_summary_text_from_member_events(
# are from explicitly to avoid, "messages in the Bob room" room_id, notifs, room_state_ids, notif_events
sender_ids = list(
{
notif_events[n["event_id"]].sender
for n in notifs_by_room[room_id]
}
) )
member_events = await self.store.get_events( async def make_summary_text(
[ self,
room_state_ids[room_id][("m.room.member", s)] notifs_by_room: Dict[str, List[Dict[str, Any]]],
for s in sender_ids room_state_ids: Dict[str, StateMap[str]],
] notif_events: Dict[str, EventBase],
) reason: Dict[str, Any],
) -> str:
"""
Make a summary text for the email when multiple rooms have notifications.
return self.email_subjects.messages_from_person % { Args:
"person": descriptor_from_member_events(member_events.values()), notifs_by_room: A map of room ID to the notifications for that room.
"app": self.app_name, room_state_ids: A map of room ID to the state map for that room.
} notif_events: A map of event ID -> notification event.
else: reason: The reason this notification is being sent.
Returns:
The summary text.
"""
# Stuff's happened in multiple different rooms # Stuff's happened in multiple different rooms
# ...but we still refer to the 'reason' room which triggered the mail # ...but we still refer to the 'reason' room which triggered the mail
if reason["room_name"] is not None: if reason["room_name"] is not None:
return self.email_subjects.messages_in_room_and_others % { return self.email_subjects.messages_in_room_and_others % {
"room": reason["room_name"], "room": reason["room_name"],
"app": self.app_name, "app": self.app_name,
} }
else:
# If the reason room doesn't have a name, say who the messages
# are from explicitly to avoid, "messages in the Bob room"
room_id = reason["room_id"]
sender_ids = list( room_id = reason["room_id"]
{ return await self.make_summary_text_from_member_events(
notif_events[n["event_id"]].sender room_id, notifs_by_room[room_id], room_state_ids[room_id], notif_events
for n in notifs_by_room[room_id]
}
) )
async def make_summary_text_from_member_events(
self,
room_id: str,
notifs: List[Dict[str, Any]],
room_state_ids: StateMap[str],
notif_events: Dict[str, EventBase],
) -> str:
"""
Make a summary text for the email when only a single room has notifications.
Args:
room_id: The ID of the room.
notifs: The notifications for this room.
room_state_ids: The state map for the room.
notif_events: A map of event ID -> notification event.
Returns:
The summary text.
"""
# If the room doesn't have a name, say who the messages
# are from explicitly to avoid, "messages in the Bob room"
sender_ids = {notif_events[n["event_id"]].sender for n in notifs}
member_events = await self.store.get_events( member_events = await self.store.get_events(
[room_state_ids[room_id][("m.room.member", s)] for s in sender_ids] [room_state_ids[("m.room.member", s)] for s in sender_ids]
) )
return self.email_subjects.messages_from_person_and_others % { # There was a single sender.
if len(sender_ids) == 1:
return self.email_subjects.messages_from_person % {
"person": descriptor_from_member_events(member_events.values()), "person": descriptor_from_member_events(member_events.values()),
"app": self.app_name, "app": self.app_name,
} }
# There was more than one sender, use the first one and a tweaked template.
return self.email_subjects.messages_from_person_and_others % {
"person": descriptor_from_member_events(list(member_events.values())[:1]),
"app": self.app_name,
}
def make_room_link(self, room_id: str) -> str: def make_room_link(self, room_id: str) -> str:
if self.hs.config.email_riot_base_url: if self.hs.config.email_riot_base_url:
base_url = "%s/#/room" % (self.hs.config.email_riot_base_url) base_url = "%s/#/room" % (self.hs.config.email_riot_base_url)

View File

@ -187,6 +187,36 @@ class EmailPusherTests(HomeserverTestCase):
# We should get emailed about those messages # We should get emailed about those messages
self._check_for_mail() self._check_for_mail()
def test_multiple_rooms(self):
# We want to test multiple notifications from multiple rooms, so we pause
# processing of push while we send messages.
self.pusher._pause_processing()
# Create a simple room with multiple other users
rooms = [
self.helper.create_room_as(self.user_id, tok=self.access_token),
self.helper.create_room_as(self.user_id, tok=self.access_token),
]
for r, other in zip(rooms, self.others):
self.helper.invite(
room=r, src=self.user_id, tok=self.access_token, targ=other.id
)
self.helper.join(room=r, user=other.id, tok=other.token)
# The other users send some messages
self.helper.send(rooms[0], body="Hi!", tok=self.others[0].token)
self.helper.send(rooms[1], body="There!", tok=self.others[1].token)
self.helper.send(rooms[1], body="There!", tok=self.others[1].token)
# Nothing should have happened yet, as we're paused.
assert not self.email_attempts
self.pusher._resume_processing()
# We should get emailed about those messages
self._check_for_mail()
def test_encrypted_message(self): def test_encrypted_message(self):
room = self.helper.create_room_as(self.user_id, tok=self.access_token) room = self.helper.create_room_as(self.user_id, tok=self.access_token)
self.helper.invite( self.helper.invite(