Coverage for app/backend/src/couchers/notifications/render_push.py: 87%
297 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
1"""
2Renders a Notification model into a localized push notification.
3"""
5import logging
6from datetime import date
7from typing import Any, assert_never
9from google.protobuf.timestamp_pb2 import Timestamp
11from couchers import urls
12from couchers.i18n import LocalizationContext
13from couchers.i18n.i18next import LocalizationError
14from couchers.i18n.localize import format_phone_number
15from couchers.models import Notification, NotificationTopicAction
16from couchers.notifications.locales import get_notifs_i18next
17from couchers.notifications.push import PushNotificationContent
18from couchers.proto import api_pb2, notification_data_pb2
20logger = logging.getLogger(__name__)
22# See PushNotificationContent's documentation for notification writing guidelines.
25def render_push_notification(notification: Notification, loc_context: LocalizationContext) -> PushNotificationContent:
26 data: Any = notification.topic_action.data_type.FromString(notification.data) # type: ignore[attr-defined]
28 match notification.topic_action:
29 # Using a match statement enable mypy's exhaustiveness checking.
30 # Every case is has its own function so that they can declare different types for "data",
31 # as mypy wouldn't allow that in a single function.
32 # Keep topics sorted (actions can follow logical ordering)
33 case NotificationTopicAction.account_deletion__start:
34 return _render_account_deletion__start(data, loc_context)
35 case NotificationTopicAction.account_deletion__complete:
36 return _render_account_deletion__complete(data, loc_context)
37 case NotificationTopicAction.account_deletion__recovered:
38 return _render_account_deletion__recovered(loc_context)
39 case NotificationTopicAction.activeness__probe:
40 return _render_activeness__probe(data, loc_context)
41 case NotificationTopicAction.api_key__create:
42 return _render_api_key__create(data, loc_context)
43 case NotificationTopicAction.badge__add:
44 return _render_badge__add(data, loc_context)
45 case NotificationTopicAction.badge__remove:
46 return _render_badge__remove(data, loc_context)
47 case NotificationTopicAction.birthdate__change:
48 return _render_birthdate__change(data, loc_context)
49 case NotificationTopicAction.chat__message:
50 return _render_chat__message(data, loc_context)
51 case NotificationTopicAction.chat__missed_messages: 51 ↛ 52line 51 didn't jump to line 52 because the pattern on line 51 never matched
52 return _render_chat__missed_messages(data, loc_context)
53 case NotificationTopicAction.donation__received:
54 return _render_donation__received(data, loc_context)
55 case NotificationTopicAction.discussion__create:
56 return _render_discussion__create(data, loc_context)
57 case NotificationTopicAction.discussion__comment:
58 return _render_discussion__comment(data, loc_context)
59 case NotificationTopicAction.email_address__change:
60 return _render_email_address__change(data, loc_context)
61 case NotificationTopicAction.email_address__verify:
62 return _render_email_address__verify(loc_context)
63 case NotificationTopicAction.event__create_any: 63 ↛ 64line 63 didn't jump to line 64 because the pattern on line 63 never matched
64 return _render_event__create_any(data, loc_context)
65 case NotificationTopicAction.event__create_approved:
66 return _render_event__create_approved(data, loc_context)
67 case NotificationTopicAction.event__update:
68 return _render_event__update(data, loc_context)
69 case NotificationTopicAction.event__invite_organizer: 69 ↛ 70line 69 didn't jump to line 70 because the pattern on line 69 never matched
70 return _render_event__invite_organizer(data, loc_context)
71 case NotificationTopicAction.event__comment:
72 return _render_event__comment(data, loc_context)
73 case NotificationTopicAction.event__reminder:
74 return _render_event__reminder(data, loc_context)
75 case NotificationTopicAction.event__cancel:
76 return _render_event__cancel(data, loc_context)
77 case NotificationTopicAction.event__delete: 77 ↛ 78line 77 didn't jump to line 78 because the pattern on line 77 never matched
78 return _render_event__delete(data, loc_context)
79 case NotificationTopicAction.friend_request__create:
80 return _render_friend_request__create(data, loc_context)
81 case NotificationTopicAction.friend_request__accept:
82 return _render_friend_request__accept(data, loc_context)
83 case NotificationTopicAction.gender__change:
84 return _render_gender__change(data, loc_context)
85 case NotificationTopicAction.general__new_blog_post:
86 return _render_general__new_blog_post(data, loc_context)
87 case NotificationTopicAction.host_request__create:
88 return _render_host_request__create(data, loc_context)
89 case NotificationTopicAction.host_request__message:
90 return _render_host_request__message(data, loc_context)
91 case NotificationTopicAction.host_request__missed_messages: 91 ↛ 92line 91 didn't jump to line 92 because the pattern on line 91 never matched
92 return _render_host_request__missed_messages(data, loc_context)
93 case NotificationTopicAction.host_request__reminder:
94 return _render_host_request__reminder(data, loc_context)
95 case NotificationTopicAction.host_request__accept:
96 return _render_host_request__accept(data, loc_context)
97 case NotificationTopicAction.host_request__reject: 97 ↛ 98line 97 didn't jump to line 98 because the pattern on line 97 never matched
98 return _render_host_request__reject(data, loc_context)
99 case NotificationTopicAction.host_request__cancel:
100 return _render_host_request__cancel(data, loc_context)
101 case NotificationTopicAction.host_request__confirm:
102 return _render_host_request__confirm(data, loc_context)
103 case NotificationTopicAction.modnote__create:
104 return _render_modnote__create(loc_context)
105 case NotificationTopicAction.onboarding__reminder:
106 return _render_onboarding__reminder(notification.key, loc_context)
107 case NotificationTopicAction.password__change:
108 return _render_password__change(loc_context)
109 case NotificationTopicAction.password_reset__start:
110 return _render_password_reset__start(data, loc_context)
111 case NotificationTopicAction.password_reset__complete:
112 return _render_password_reset__complete(loc_context)
113 case NotificationTopicAction.phone_number__change:
114 return _render_phone_number__change(data, loc_context)
115 case NotificationTopicAction.phone_number__verify:
116 return _render_phone_number__verify(data, loc_context)
117 case NotificationTopicAction.postal_verification__postcard_sent:
118 return _render_postal_verification__postcard_sent(data, loc_context)
119 case NotificationTopicAction.postal_verification__success: 119 ↛ 120line 119 didn't jump to line 120 because the pattern on line 119 never matched
120 return _render_postal_verification__success(loc_context)
121 case NotificationTopicAction.postal_verification__failed: 121 ↛ 122line 121 didn't jump to line 122 because the pattern on line 121 never matched
122 return _render_postal_verification__failed(data, loc_context)
123 case NotificationTopicAction.reference__receive_friend:
124 return _render_reference__receive_friend(data, loc_context)
125 case NotificationTopicAction.reference__receive_hosted: 125 ↛ 126line 125 didn't jump to line 126 because the pattern on line 125 never matched
126 return _render_reference__receive_hosted(data, loc_context)
127 case NotificationTopicAction.reference__receive_surfed:
128 return _render_reference__receive_surfed(data, loc_context)
129 case NotificationTopicAction.reference__reminder_hosted:
130 return _render_reference__reminder_hosted(data, loc_context)
131 case NotificationTopicAction.reference__reminder_surfed:
132 return _render_reference__reminder_surfed(data, loc_context)
133 case NotificationTopicAction.thread__reply:
134 return _render_thread__reply(data, loc_context)
135 case NotificationTopicAction.verification__sv_success:
136 return _render_verification__sv_success(loc_context)
137 case NotificationTopicAction.verification__sv_fail: 137 ↛ 139line 137 didn't jump to line 139 because the pattern on line 137 always matched
138 return _render_verification__sv_fail(data, loc_context)
139 case _:
140 # Enables mypy's exhaustiveness checking for the cases above.
141 assert_never(notification.topic_action)
144def render_adhoc_push_notification(name: str, loc_context: LocalizationContext) -> PushNotificationContent:
145 """Renders a push notification that doesn't have an assigned topic-action."""
146 return _get_content(string_group=f"_adhoc.{name}.push", loc_context=loc_context)
149def _get_content(
150 string_group: NotificationTopicAction | str,
151 loc_context: LocalizationContext,
152 title: str | None = None,
153 ios_title: str | None = None,
154 ios_subtitle: str | None = None,
155 body: str | None = None,
156 substitutions: dict[str, str | int] | None = None,
157 icon_user: api_pb2.User | None = None,
158 action_url: str | None = None,
159) -> PushNotificationContent:
160 """
161 Fills a PushNotificationContent by looking up localized
162 string based on the topic_action key, unless other strings
163 are provided by the caller.
165 Localized strings have the provided substitutions applied.
166 """
167 # Look up the localized string for any string that was not provided
168 if title is None: 168 ↛ 170line 168 didn't jump to line 170 because the condition on line 168 was always true
169 title = _get_string(string_group, "title", loc_context, substitutions)
170 if ios_title is None:
171 ios_title = _get_string(string_group, "ios_title", loc_context, substitutions)
172 if ios_subtitle is None:
173 try:
174 ios_subtitle = _get_string(string_group, "ios_subtitle", loc_context, substitutions)
175 except LocalizationError:
176 # Not all notifications have subtitles
177 pass
178 if body is None:
179 body = _get_string(string_group, "body", loc_context, substitutions)
181 icon_url = _avatar_url_or_default(icon_user) if icon_user else None
183 return PushNotificationContent(
184 title=title, ios_title=ios_title, ios_subtitle=ios_subtitle, body=body, icon_url=icon_url, action_url=action_url
185 )
188def _get_string(
189 string_group: NotificationTopicAction | str,
190 key: str,
191 loc_context: LocalizationContext,
192 substitutions: dict[str, str | int] | None = None,
193) -> str:
194 if isinstance(string_group, NotificationTopicAction):
195 full_key = f"{string_group.topic}.{string_group.action}.push.{key}"
196 else:
197 full_key = f"{string_group}.{key}"
198 return get_notifs_i18next().localize(full_key, loc_context.locale, substitutions)
201def _avatar_url_or_default(user: api_pb2.User) -> str:
202 return user.avatar_thumbnail_url or urls.icon_url()
205def _render_account_deletion__start(
206 data: notification_data_pb2.AccountDeletionStart, loc_context: LocalizationContext
207) -> PushNotificationContent:
208 return _get_content(NotificationTopicAction.account_deletion__start, loc_context)
211def _render_account_deletion__complete(
212 data: notification_data_pb2.AccountDeletionComplete, loc_context: LocalizationContext
213) -> PushNotificationContent:
214 return _get_content(
215 NotificationTopicAction.account_deletion__complete, loc_context, substitutions={"count": data.undelete_days}
216 )
219def _render_account_deletion__recovered(loc_context: LocalizationContext) -> PushNotificationContent:
220 return _get_content(NotificationTopicAction.account_deletion__recovered, loc_context)
223def _render_activeness__probe(
224 data: notification_data_pb2.ActivenessProbe, loc_context: LocalizationContext
225) -> PushNotificationContent:
226 return _get_content(NotificationTopicAction.activeness__probe, loc_context)
229def _render_api_key__create(
230 data: notification_data_pb2.ApiKeyCreate, loc_context: LocalizationContext
231) -> PushNotificationContent:
232 return _get_content(NotificationTopicAction.api_key__create, loc_context)
235def _render_badge__add(
236 data: notification_data_pb2.BadgeAdd, loc_context: LocalizationContext
237) -> PushNotificationContent:
238 return _get_content(
239 NotificationTopicAction.badge__add,
240 loc_context,
241 substitutions={"badge_name": data.badge_name},
242 action_url=urls.profile_link(),
243 )
246def _render_badge__remove(
247 data: notification_data_pb2.BadgeRemove, loc_context: LocalizationContext
248) -> PushNotificationContent:
249 return _get_content(
250 NotificationTopicAction.badge__remove,
251 loc_context,
252 substitutions={"badge_name": data.badge_name},
253 action_url=urls.profile_link(),
254 )
257def _render_birthdate__change(
258 data: notification_data_pb2.BirthdateChange, loc_context: LocalizationContext
259) -> PushNotificationContent:
260 return _get_content(
261 NotificationTopicAction.birthdate__change,
262 loc_context,
263 substitutions={"birthdate": loc_context.localize_date_from_iso(data.birthdate)},
264 action_url=urls.account_settings_link(),
265 )
268def _render_chat__message(
269 data: notification_data_pb2.ChatMessage, loc_context: LocalizationContext
270) -> PushNotificationContent:
271 # All strings are dynamic, no need to use _get_content
272 return PushNotificationContent(
273 title=data.author.name,
274 ios_title=data.author.name,
275 body=data.text,
276 icon_url=_avatar_url_or_default(data.author),
277 action_url=urls.chat_link(chat_id=data.group_chat_id),
278 )
281def _render_chat__missed_messages(
282 data: notification_data_pb2.ChatMissedMessages, loc_context: LocalizationContext
283) -> PushNotificationContent:
284 # Each message is from a different chat, so this counts conversations.
285 missed_count: int = len(data.messages)
287 # Newer version of protos include a per-chat unseen message count (1 or more)
288 if data.messages and data.messages[0].unseen_count:
289 missed_count = sum(message.unseen_count for message in data.messages)
291 return _get_content(
292 NotificationTopicAction.chat__missed_messages,
293 loc_context,
294 substitutions={"count": missed_count},
295 action_url=urls.messages_link(),
296 )
299def _render_donation__received(
300 data: notification_data_pb2.DonationReceived, loc_context: LocalizationContext
301) -> PushNotificationContent:
302 return _get_content(
303 NotificationTopicAction.donation__received,
304 loc_context,
305 # Other currencies are not yet supported
306 substitutions={"amount_with_currency": f"${data.amount}"},
307 action_url=data.receipt_url,
308 )
311def _render_discussion__create(
312 data: notification_data_pb2.DiscussionCreate, loc_context: LocalizationContext
313) -> PushNotificationContent:
314 return _get_content(
315 NotificationTopicAction.discussion__create,
316 loc_context,
317 substitutions={
318 "title": data.discussion.title,
319 "user": data.author.name,
320 "group_or_community": data.discussion.owner_title,
321 },
322 icon_user=data.author,
323 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug),
324 )
327def _render_discussion__comment(
328 data: notification_data_pb2.DiscussionComment, loc_context: LocalizationContext
329) -> PushNotificationContent:
330 return _get_content(
331 NotificationTopicAction.discussion__comment,
332 loc_context,
333 ios_title=data.author.name,
334 ios_subtitle=data.discussion.title,
335 body=data.reply.content,
336 substitutions={"user": data.author.name, "title": data.discussion.title},
337 icon_user=data.author,
338 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug),
339 )
342def _render_email_address__change(
343 data: notification_data_pb2.EmailAddressChange, loc_context: LocalizationContext
344) -> PushNotificationContent:
345 return _get_content(
346 NotificationTopicAction.email_address__change,
347 loc_context,
348 substitutions={"email": data.new_email},
349 action_url=urls.account_settings_link(),
350 )
353def _render_email_address__verify(loc_context: LocalizationContext) -> PushNotificationContent:
354 return _get_content(
355 NotificationTopicAction.email_address__verify,
356 loc_context,
357 action_url=urls.account_settings_link(),
358 )
361def _render_event__create_any(
362 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext
363) -> PushNotificationContent:
364 return _get_content(
365 NotificationTopicAction.event__create_any,
366 loc_context,
367 substitutions={
368 "title": data.event.title,
369 "user": data.inviting_user.name,
370 "date_and_time": _format_event_start_datetime(data.event.start_time, loc_context),
371 },
372 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
373 )
376def _render_event__create_approved(
377 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext
378) -> PushNotificationContent:
379 return _get_content(
380 NotificationTopicAction.event__create_approved,
381 loc_context,
382 substitutions={
383 "title": data.event.title,
384 "user": data.inviting_user.name,
385 "date_and_time": _format_event_start_datetime(data.event.start_time, loc_context),
386 },
387 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
388 )
391def _render_event__update(
392 data: notification_data_pb2.EventUpdate, loc_context: LocalizationContext
393) -> PushNotificationContent:
394 # updated_items can include: title, content, start_time, end_time, location,
395 # but a list like that is tricky to localize.
396 return _get_content(
397 NotificationTopicAction.event__update,
398 loc_context,
399 substitutions={
400 "title": data.event.title,
401 "user": data.updating_user.name,
402 },
403 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
404 )
407def _render_event__invite_organizer(
408 data: notification_data_pb2.EventInviteOrganizer, loc_context: LocalizationContext
409) -> PushNotificationContent:
410 return _get_content(
411 NotificationTopicAction.event__invite_organizer,
412 loc_context,
413 substitutions={
414 "title": data.event.title,
415 "user": data.inviting_user.name,
416 },
417 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
418 )
421def _render_event__comment(
422 data: notification_data_pb2.EventComment, loc_context: LocalizationContext
423) -> PushNotificationContent:
424 return _get_content(
425 NotificationTopicAction.event__comment,
426 loc_context,
427 substitutions={
428 "title": data.event.title,
429 "user": data.author.name,
430 },
431 body=data.reply.content,
432 icon_user=data.author,
433 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
434 )
437def _render_event__reminder(
438 data: notification_data_pb2.EventReminder, loc_context: LocalizationContext
439) -> PushNotificationContent:
440 return _get_content(
441 NotificationTopicAction.event__reminder,
442 loc_context,
443 substitutions={
444 "title": data.event.title,
445 "date_and_time": _format_event_start_datetime(data.event.start_time, loc_context),
446 },
447 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
448 )
451def _render_event__cancel(
452 data: notification_data_pb2.EventCancel, loc_context: LocalizationContext
453) -> PushNotificationContent:
454 return _get_content(
455 NotificationTopicAction.event__cancel,
456 loc_context,
457 substitutions={
458 "title": data.event.title,
459 "user": data.cancelling_user.name,
460 },
461 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
462 )
465def _render_event__delete(
466 data: notification_data_pb2.EventDelete, loc_context: LocalizationContext
467) -> PushNotificationContent:
468 return _get_content(NotificationTopicAction.event__delete, loc_context, substitutions={"title": data.event.title})
471def _render_friend_request__create(
472 data: notification_data_pb2.FriendRequestCreate, loc_context: LocalizationContext
473) -> PushNotificationContent:
474 return _get_content(
475 NotificationTopicAction.friend_request__create,
476 loc_context,
477 substitutions={"from_user": data.other_user.name},
478 icon_user=data.other_user,
479 action_url=urls.friend_requests_link(from_user_id=data.other_user.user_id),
480 )
483def _render_friend_request__accept(
484 data: notification_data_pb2.FriendRequestAccept, loc_context: LocalizationContext
485) -> PushNotificationContent:
486 return _get_content(
487 NotificationTopicAction.friend_request__accept,
488 loc_context,
489 substitutions={"friend": data.other_user.name},
490 icon_user=data.other_user,
491 action_url=urls.user_link(username=data.other_user.username),
492 )
495def _render_gender__change(
496 data: notification_data_pb2.GenderChange, loc_context: LocalizationContext
497) -> PushNotificationContent:
498 return _get_content(
499 NotificationTopicAction.gender__change,
500 loc_context,
501 substitutions={"gender": data.gender},
502 action_url=urls.account_settings_link(),
503 )
506def _render_general__new_blog_post(
507 data: notification_data_pb2.GeneralNewBlogPost, loc_context: LocalizationContext
508) -> PushNotificationContent:
509 return _get_content(
510 NotificationTopicAction.general__new_blog_post,
511 loc_context,
512 body=data.blurb,
513 substitutions={"title": data.title},
514 action_url=data.url,
515 )
518def _render_host_request__create(
519 data: notification_data_pb2.HostRequestCreate, loc_context: LocalizationContext
520) -> PushNotificationContent:
521 night_count = (date.fromisoformat(data.host_request.to_date) - date.fromisoformat(data.host_request.from_date)).days
522 return _get_content(
523 NotificationTopicAction.host_request__create,
524 loc_context,
525 substitutions={
526 "user": data.surfer.name,
527 "start_date": _format_host_request_start_date(data.host_request.from_date, loc_context),
528 "count": night_count,
529 },
530 icon_user=data.surfer,
531 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
532 )
535def _render_host_request__message(
536 data: notification_data_pb2.HostRequestMessage, loc_context: LocalizationContext
537) -> PushNotificationContent:
538 # All strings are dynamic, no need to use _get_content
539 return PushNotificationContent(
540 title=data.user.name,
541 ios_title=data.user.name,
542 body=data.text,
543 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
544 icon_url=_avatar_url_or_default(data.user),
545 )
548def _render_host_request__missed_messages(
549 data: notification_data_pb2.HostRequestMissedMessages, loc_context: LocalizationContext
550) -> PushNotificationContent:
551 return _get_content(
552 NotificationTopicAction.host_request__missed_messages,
553 loc_context,
554 substitutions={"user": data.user.name},
555 icon_user=data.user,
556 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
557 )
560def _render_host_request__reminder(
561 data: notification_data_pb2.HostRequestReminder, loc_context: LocalizationContext
562) -> PushNotificationContent:
563 return _get_content(
564 NotificationTopicAction.host_request__reminder,
565 loc_context,
566 substitutions={"user": data.surfer.name},
567 icon_user=data.surfer,
568 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
569 )
572def _render_host_request__accept(
573 data: notification_data_pb2.HostRequestAccept, loc_context: LocalizationContext
574) -> PushNotificationContent:
575 return _get_content(
576 NotificationTopicAction.host_request__accept,
577 loc_context,
578 substitutions={
579 "user": data.host.name,
580 "date": _format_host_request_start_date(data.host_request.from_date, loc_context),
581 },
582 icon_user=data.host,
583 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
584 )
587def _render_host_request__reject(
588 data: notification_data_pb2.HostRequestReject, loc_context: LocalizationContext
589) -> PushNotificationContent:
590 return _get_content(
591 NotificationTopicAction.host_request__reject,
592 loc_context,
593 substitutions={
594 "user": data.host.name,
595 "date": _format_host_request_start_date(data.host_request.from_date, loc_context),
596 },
597 icon_user=data.host,
598 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
599 )
602def _render_host_request__cancel(
603 data: notification_data_pb2.HostRequestCancel, loc_context: LocalizationContext
604) -> PushNotificationContent:
605 return _get_content(
606 NotificationTopicAction.host_request__cancel,
607 loc_context,
608 substitutions={
609 "user": data.surfer.name,
610 "date": _format_host_request_start_date(data.host_request.from_date, loc_context),
611 },
612 icon_user=data.surfer,
613 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
614 )
617def _render_host_request__confirm(
618 data: notification_data_pb2.HostRequestConfirm, loc_context: LocalizationContext
619) -> PushNotificationContent:
620 return _get_content(
621 NotificationTopicAction.host_request__confirm,
622 loc_context,
623 substitutions={
624 "user": data.surfer.name,
625 "date": _format_host_request_start_date(data.host_request.from_date, loc_context),
626 },
627 icon_user=data.surfer,
628 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
629 )
632def _render_modnote__create(loc_context: LocalizationContext) -> PushNotificationContent:
633 return _get_content(NotificationTopicAction.modnote__create, loc_context)
636def _render_onboarding__reminder(key: str, loc_context: LocalizationContext) -> PushNotificationContent:
637 variant = "first" if key == "1" else "subsequent"
638 return _get_content(
639 f"onboarding.reminder.push.{variant}",
640 loc_context,
641 action_url=urls.edit_profile_link(),
642 )
645def _render_password__change(loc_context: LocalizationContext) -> PushNotificationContent:
646 return _get_content(NotificationTopicAction.password__change, loc_context, action_url=urls.account_settings_link())
649def _render_password_reset__start(
650 data: notification_data_pb2.PasswordResetStart, loc_context: LocalizationContext
651) -> PushNotificationContent:
652 return _get_content(
653 NotificationTopicAction.password_reset__start,
654 loc_context,
655 action_url=urls.account_settings_link(),
656 )
659def _render_password_reset__complete(loc_context: LocalizationContext) -> PushNotificationContent:
660 return _get_content(
661 NotificationTopicAction.password_reset__complete,
662 loc_context,
663 action_url=urls.account_settings_link(),
664 )
667def _render_phone_number__change(
668 data: notification_data_pb2.PhoneNumberChange, loc_context: LocalizationContext
669) -> PushNotificationContent:
670 return _get_content(
671 NotificationTopicAction.phone_number__change,
672 loc_context,
673 substitutions={"phone_number": format_phone_number(data.phone)},
674 action_url=urls.account_settings_link(),
675 )
678def _render_phone_number__verify(
679 data: notification_data_pb2.PhoneNumberVerify, loc_context: LocalizationContext
680) -> PushNotificationContent:
681 return _get_content(
682 NotificationTopicAction.phone_number__verify,
683 loc_context,
684 substitutions={"phone_number": format_phone_number(data.phone)},
685 action_url=urls.account_settings_link(),
686 )
689def _render_postal_verification__postcard_sent(
690 data: notification_data_pb2.PostalVerificationPostcardSent, loc_context: LocalizationContext
691) -> PushNotificationContent:
692 return _get_content(
693 NotificationTopicAction.postal_verification__postcard_sent,
694 loc_context,
695 substitutions={"city": data.city, "country": data.country},
696 action_url=urls.account_settings_link(),
697 )
700def _render_postal_verification__success(loc_context: LocalizationContext) -> PushNotificationContent:
701 return _get_content(
702 NotificationTopicAction.postal_verification__success,
703 loc_context,
704 action_url=urls.account_settings_link(),
705 )
708def _render_postal_verification__failed(
709 data: notification_data_pb2.PostalVerificationFailed, loc_context: LocalizationContext
710) -> PushNotificationContent:
711 body_key: str
712 match data.reason:
713 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_CODE_EXPIRED:
714 body_key = "body_code_expired"
715 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_TOO_MANY_ATTEMPTS:
716 body_key = "body_too_many_attempts"
717 case _:
718 body_key = "body_generic"
720 return _get_content(
721 NotificationTopicAction.postal_verification__failed,
722 loc_context,
723 body=_get_string(NotificationTopicAction.postal_verification__failed, body_key, loc_context),
724 action_url=urls.account_settings_link(),
725 )
728def _render_reference__receive_friend(
729 data: notification_data_pb2.ReferenceReceiveFriend, loc_context: LocalizationContext
730) -> PushNotificationContent:
731 return _get_content(
732 NotificationTopicAction.reference__receive_friend,
733 loc_context,
734 body=data.text,
735 substitutions={"user": data.from_user.name},
736 icon_user=data.from_user,
737 action_url=urls.profile_references_link(),
738 )
741def _render_reference__receive(
742 data: notification_data_pb2.ReferenceReceiveHostRequest, leave_reference_type: str, loc_context: LocalizationContext
743) -> PushNotificationContent:
744 body: str
745 if data.text: 745 ↛ 746line 745 didn't jump to line 746 because the condition on line 745 was never true
746 body = data.text
747 action_url = urls.profile_references_link()
748 else:
749 body = _get_string(
750 "reference._receive_any.push",
751 "body_must_write_yours",
752 loc_context,
753 substitutions={"user": data.from_user.name},
754 )
755 action_url = urls.leave_reference_link(
756 reference_type=leave_reference_type,
757 to_user_id=data.from_user.user_id,
758 host_request_id=str(data.host_request_id),
759 )
760 return _get_content(
761 string_group="reference._receive_any.push",
762 loc_context=loc_context,
763 body=body,
764 substitutions={"user": data.from_user.name},
765 icon_user=data.from_user,
766 action_url=action_url,
767 )
770def _render_reference__receive_hosted(
771 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext
772) -> PushNotificationContent:
773 # Receiving a hosted reminder means I need to leave a surfed reference
774 return _render_reference__receive(data, leave_reference_type="surfed", loc_context=loc_context)
777def _render_reference__receive_surfed(
778 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext
779) -> PushNotificationContent:
780 return _render_reference__receive(data, leave_reference_type="hosted", loc_context=loc_context)
783def _render_reference__reminder(
784 data: notification_data_pb2.ReferenceReminder, leave_reference_type: str, loc_context: LocalizationContext
785) -> PushNotificationContent:
786 leave_reference_link = urls.leave_reference_link(
787 reference_type=leave_reference_type,
788 to_user_id=data.other_user.user_id,
789 host_request_id=str(data.host_request_id),
790 )
791 return _get_content(
792 string_group="reference._reminder_any.push",
793 loc_context=loc_context,
794 substitutions={"count": data.days_left, "user": data.other_user.name},
795 icon_user=data.other_user,
796 action_url=leave_reference_link,
797 )
800def _render_reference__reminder_surfed(
801 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext
802) -> PushNotificationContent:
803 # Surfed reminder means I need to leave a surfed reference
804 return _render_reference__reminder(data, leave_reference_type="surfed", loc_context=loc_context)
807def _render_reference__reminder_hosted(
808 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext
809) -> PushNotificationContent:
810 return _render_reference__reminder(data, leave_reference_type="hosted", loc_context=loc_context)
813def _render_thread__reply(
814 data: notification_data_pb2.ThreadReply, loc_context: LocalizationContext
815) -> PushNotificationContent:
816 parent_title: str
817 view_link: str
818 match data.WhichOneof("reply_parent"):
819 case "event":
820 parent_title = data.event.title
821 view_link = urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug)
822 case "discussion": 822 ↛ 825line 822 didn't jump to line 825 because the pattern on line 822 always matched
823 parent_title = data.discussion.title
824 view_link = urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug)
825 case _:
826 raise Exception("Can only do replies to events and discussions")
828 return _get_content(
829 NotificationTopicAction.thread__reply,
830 loc_context=loc_context,
831 body=data.reply.content,
832 substitutions={"user": data.author.name, "title": parent_title},
833 icon_user=data.author,
834 action_url=view_link,
835 )
838def _render_verification__sv_success(loc_context: LocalizationContext) -> PushNotificationContent:
839 return _get_content(
840 NotificationTopicAction.verification__sv_success,
841 loc_context,
842 action_url=urls.account_settings_link(),
843 )
846def _render_verification__sv_fail(
847 data: notification_data_pb2.VerificationSVFail, loc_context: LocalizationContext
848) -> PushNotificationContent:
849 body_key: str
850 match data.reason:
851 case notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER: 851 ↛ 852line 851 didn't jump to line 852 because the pattern on line 851 never matched
852 body_key = "body_wrong_birthdate_gender"
853 case notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT:
854 body_key = "body_not_a_passport"
855 case notification_data_pb2.SV_FAIL_REASON_DUPLICATE: 855 ↛ 857line 855 didn't jump to line 857 because the pattern on line 855 always matched
856 body_key = "body_duplicate"
857 case _:
858 raise Exception("Shouldn't get here")
860 return _get_content(
861 NotificationTopicAction.verification__sv_fail,
862 loc_context,
863 body=_get_string(NotificationTopicAction.verification__sv_fail, body_key, loc_context),
864 action_url=urls.account_settings_link(),
865 )
868def _format_host_request_start_date(date: str, loc_context: LocalizationContext) -> str:
869 # Events are typically in the near future future,
870 # so the year is not useful but the day of week is.
871 return loc_context.localize_date_from_iso(date, with_year=False, with_day_of_week=True)
874def _format_event_start_datetime(timestamp: Timestamp, loc_context: LocalizationContext) -> str:
875 # Events are typically in the near future future,
876 # so the year is not useful but the day of week is.
877 return loc_context.localize_datetime(timestamp, with_year=False, with_day_of_week=True)