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