Coverage for app / backend / src / couchers / notifications / render_push.py: 87%
291 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +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 couchers import urls
10from couchers.i18n import LocalizationContext
11from couchers.i18n.i18next import LocalizationError
12from couchers.i18n.localize import format_phone_number
13from couchers.models import Notification, NotificationTopicAction
14from couchers.notifications.locales import get_notifs_i18next
15from couchers.notifications.push import PushNotificationContent
16from couchers.proto import api_pb2, notification_data_pb2
18logger = logging.getLogger(__name__)
20# See PushNotificationContent's documentation for notification writing guidelines.
23def render_push_notification(notification: Notification, loc_context: LocalizationContext) -> PushNotificationContent:
24 data: Any = notification.topic_action.data_type.FromString(notification.data) # type: ignore[attr-defined]
26 match notification.topic_action:
27 # Using a match statement enable mypy's exhaustiveness checking.
28 # Every case is has its own function so that they can declare different types for "data",
29 # as mypy wouldn't allow that in a single function.
30 # Keep topics sorted (actions can follow logical ordering)
31 case NotificationTopicAction.account_deletion__start:
32 return _render_account_deletion__start(data, loc_context)
33 case NotificationTopicAction.account_deletion__complete:
34 return _render_account_deletion__complete(data, loc_context)
35 case NotificationTopicAction.account_deletion__recovered:
36 return _render_account_deletion__recovered(loc_context)
37 case NotificationTopicAction.activeness__probe:
38 return _render_activeness__probe(data, loc_context)
39 case NotificationTopicAction.api_key__create:
40 return _render_api_key__create(data, loc_context)
41 case NotificationTopicAction.badge__add:
42 return _render_badge__add(data, loc_context)
43 case NotificationTopicAction.badge__remove:
44 return _render_badge__remove(data, loc_context)
45 case NotificationTopicAction.birthdate__change:
46 return _render_birthdate__change(data, loc_context)
47 case NotificationTopicAction.chat__message:
48 return _render_chat__message(data, loc_context)
49 case NotificationTopicAction.chat__missed_messages: 49 ↛ 50line 49 didn't jump to line 50 because the pattern on line 49 never matched
50 return _render_chat__missed_messages(data, loc_context)
51 case NotificationTopicAction.donation__received:
52 return _render_donation__received(data, loc_context)
53 case NotificationTopicAction.discussion__create:
54 return _render_discussion__create(data, loc_context)
55 case NotificationTopicAction.discussion__comment:
56 return _render_discussion__comment(data, loc_context)
57 case NotificationTopicAction.email_address__change:
58 return _render_email_address__change(data, loc_context)
59 case NotificationTopicAction.email_address__verify:
60 return _render_email_address__verify(loc_context)
61 case NotificationTopicAction.event__create_any: 61 ↛ 62line 61 didn't jump to line 62 because the pattern on line 61 never matched
62 return _render_event__create_any(data, loc_context)
63 case NotificationTopicAction.event__create_approved:
64 return _render_event__create_approved(data, loc_context)
65 case NotificationTopicAction.event__update:
66 return _render_event__update(data, loc_context)
67 case NotificationTopicAction.event__invite_organizer: 67 ↛ 68line 67 didn't jump to line 68 because the pattern on line 67 never matched
68 return _render_event__invite_organizer(data, loc_context)
69 case NotificationTopicAction.event__comment:
70 return _render_event__comment(data, loc_context)
71 case NotificationTopicAction.event__reminder:
72 return _render_event__reminder(data, loc_context)
73 case NotificationTopicAction.event__cancel:
74 return _render_event__cancel(data, loc_context)
75 case NotificationTopicAction.event__delete: 75 ↛ 76line 75 didn't jump to line 76 because the pattern on line 75 never matched
76 return _render_event__delete(data, loc_context)
77 case NotificationTopicAction.friend_request__create:
78 return _render_friend_request__create(data, loc_context)
79 case NotificationTopicAction.friend_request__accept:
80 return _render_friend_request__accept(data, loc_context)
81 case NotificationTopicAction.gender__change:
82 return _render_gender__change(data, loc_context)
83 case NotificationTopicAction.general__new_blog_post:
84 return _render_general__new_blog_post(data, loc_context)
85 case NotificationTopicAction.host_request__create:
86 return _render_host_request__create(data, loc_context)
87 case NotificationTopicAction.host_request__message:
88 return _render_host_request__message(data, loc_context)
89 case NotificationTopicAction.host_request__missed_messages: 89 ↛ 90line 89 didn't jump to line 90 because the pattern on line 89 never matched
90 return _render_host_request__missed_messages(data, loc_context)
91 case NotificationTopicAction.host_request__reminder:
92 return _render_host_request__reminder(data, loc_context)
93 case NotificationTopicAction.host_request__accept:
94 return _render_host_request__accept(data, loc_context)
95 case NotificationTopicAction.host_request__reject: 95 ↛ 96line 95 didn't jump to line 96 because the pattern on line 95 never matched
96 return _render_host_request__reject(data, loc_context)
97 case NotificationTopicAction.host_request__cancel: 97 ↛ 98line 97 didn't jump to line 98 because the pattern on line 97 never matched
98 return _render_host_request__cancel(data, loc_context)
99 case NotificationTopicAction.host_request__confirm:
100 return _render_host_request__confirm(data, loc_context)
101 case NotificationTopicAction.modnote__create:
102 return _render_modnote__create(loc_context)
103 case NotificationTopicAction.onboarding__reminder:
104 return _render_onboarding__reminder(notification.key, loc_context)
105 case NotificationTopicAction.password__change:
106 return _render_password__change(loc_context)
107 case NotificationTopicAction.password_reset__start:
108 return _render_password_reset__start(data, loc_context)
109 case NotificationTopicAction.password_reset__complete:
110 return _render_password_reset__complete(loc_context)
111 case NotificationTopicAction.phone_number__change:
112 return _render_phone_number__change(data, loc_context)
113 case NotificationTopicAction.phone_number__verify:
114 return _render_phone_number__verify(data, loc_context)
115 case NotificationTopicAction.postal_verification__postcard_sent:
116 return _render_postal_verification__postcard_sent(data, loc_context)
117 case NotificationTopicAction.postal_verification__success: 117 ↛ 118line 117 didn't jump to line 118 because the pattern on line 117 never matched
118 return _render_postal_verification__success(loc_context)
119 case NotificationTopicAction.postal_verification__failed: 119 ↛ 120line 119 didn't jump to line 120 because the pattern on line 119 never matched
120 return _render_postal_verification__failed(data, loc_context)
121 case NotificationTopicAction.reference__receive_friend:
122 return _render_reference__receive_friend(data, loc_context)
123 case NotificationTopicAction.reference__receive_hosted: 123 ↛ 124line 123 didn't jump to line 124 because the pattern on line 123 never matched
124 return _render_reference__receive_hosted(data, loc_context)
125 case NotificationTopicAction.reference__receive_surfed:
126 return _render_reference__receive_surfed(data, loc_context)
127 case NotificationTopicAction.reference__reminder_hosted:
128 return _render_reference__reminder_hosted(data, loc_context)
129 case NotificationTopicAction.reference__reminder_surfed:
130 return _render_reference__reminder_surfed(data, loc_context)
131 case NotificationTopicAction.thread__reply:
132 return _render_thread__reply(data, loc_context)
133 case NotificationTopicAction.verification__sv_success:
134 return _render_verification__sv_success(loc_context)
135 case NotificationTopicAction.verification__sv_fail: 135 ↛ 137line 135 didn't jump to line 137 because the pattern on line 135 always matched
136 return _render_verification__sv_fail(data, loc_context)
137 case _:
138 # Enables mypy's exhaustiveness checking for the cases above.
139 assert_never(notification.topic_action)
142def render_adhoc_push_notification(name: str, loc_context: LocalizationContext) -> PushNotificationContent:
143 """Renders a push notification that doesn't have an assigned topic-action."""
144 return _get_content(string_group=f"adhoc__{name}", loc_context=loc_context)
147def _get_content(
148 string_group: NotificationTopicAction | str,
149 loc_context: LocalizationContext,
150 title: str | None = None,
151 ios_title: str | None = None,
152 ios_subtitle: str | None = None,
153 body: str | None = None,
154 substitutions: dict[str, str | int] | None = None,
155 icon_user: api_pb2.User | None = None,
156 action_url: str | None = None,
157) -> PushNotificationContent:
158 """
159 Fills a PushNotificationContent by looking up localized
160 string based on the topic_action key, unless other strings
161 are provided by the caller.
163 Localized strings have the provided substitutions applied.
164 """
165 # Look up the localized string for any string that was not provided
166 if title is None: 166 ↛ 168line 166 didn't jump to line 168 because the condition on line 166 was always true
167 title = _get_string(string_group, "title", loc_context, substitutions)
168 if ios_title is None:
169 ios_title = _get_string(string_group, "ios_title", loc_context, substitutions)
170 if ios_subtitle is None:
171 try:
172 ios_subtitle = _get_string(string_group, "ios_subtitle", loc_context, substitutions)
173 except LocalizationError:
174 # Not all notifications have subtitles
175 pass
176 if body is None:
177 body = _get_string(string_group, "body", loc_context, substitutions)
179 icon_url = _avatar_url_or_default(icon_user) if icon_user else None
181 return PushNotificationContent(
182 title=title, ios_title=title, ios_subtitle=ios_subtitle, body=body, icon_url=icon_url, action_url=action_url
183 )
186def _get_string(
187 string_group: NotificationTopicAction | str,
188 key: str,
189 loc_context: LocalizationContext,
190 substitutions: dict[str, str | int] | None = None,
191) -> str:
192 if isinstance(string_group, NotificationTopicAction):
193 string_group = string_group.display.replace(":", "__")
194 key = f"push.{string_group}.{key}"
195 return get_notifs_i18next().localize(key, loc_context.locale, substitutions)
198def _avatar_url_or_default(user: api_pb2.User) -> str:
199 return user.avatar_thumbnail_url or urls.icon_url()
202def _render_account_deletion__start(
203 data: notification_data_pb2.AccountDeletionStart, loc_context: LocalizationContext
204) -> PushNotificationContent:
205 return _get_content(NotificationTopicAction.account_deletion__start, loc_context)
208def _render_account_deletion__complete(
209 data: notification_data_pb2.AccountDeletionComplete, loc_context: LocalizationContext
210) -> PushNotificationContent:
211 return _get_content(
212 NotificationTopicAction.account_deletion__complete, loc_context, substitutions={"count": data.undelete_days}
213 )
216def _render_account_deletion__recovered(loc_context: LocalizationContext) -> PushNotificationContent:
217 return _get_content(NotificationTopicAction.account_deletion__recovered, loc_context)
220def _render_activeness__probe(
221 data: notification_data_pb2.ActivenessProbe, loc_context: LocalizationContext
222) -> PushNotificationContent:
223 return _get_content(NotificationTopicAction.activeness__probe, loc_context)
226def _render_api_key__create(
227 data: notification_data_pb2.ApiKeyCreate, loc_context: LocalizationContext
228) -> PushNotificationContent:
229 return _get_content(NotificationTopicAction.api_key__create, loc_context)
232def _render_badge__add(
233 data: notification_data_pb2.BadgeAdd, loc_context: LocalizationContext
234) -> PushNotificationContent:
235 return _get_content(
236 NotificationTopicAction.badge__add,
237 loc_context,
238 substitutions={"badge_name": data.badge_name},
239 action_url=urls.profile_link(),
240 )
243def _render_badge__remove(
244 data: notification_data_pb2.BadgeRemove, loc_context: LocalizationContext
245) -> PushNotificationContent:
246 return _get_content(
247 NotificationTopicAction.badge__remove,
248 loc_context,
249 substitutions={"badge_name": data.badge_name},
250 action_url=urls.profile_link(),
251 )
254def _render_birthdate__change(
255 data: notification_data_pb2.BirthdateChange, loc_context: LocalizationContext
256) -> PushNotificationContent:
257 return _get_content(
258 NotificationTopicAction.birthdate__change,
259 loc_context,
260 substitutions={"birthdate": loc_context.localize_date_from_iso(data.birthdate)},
261 action_url=urls.account_settings_link(),
262 )
265def _render_chat__message(
266 data: notification_data_pb2.ChatMessage, loc_context: LocalizationContext
267) -> PushNotificationContent:
268 # All strings are dynamic, no need to use _get_content
269 return PushNotificationContent(
270 title=data.author.name,
271 ios_title=data.author.name,
272 body=data.text,
273 icon_url=_avatar_url_or_default(data.author),
274 action_url=urls.chat_link(chat_id=data.group_chat_id),
275 )
278def _render_chat__missed_messages(
279 data: notification_data_pb2.ChatMissedMessages, loc_context: LocalizationContext
280) -> PushNotificationContent:
281 return _get_content(
282 NotificationTopicAction.chat__missed_messages,
283 loc_context,
284 substitutions={"count": len(data.messages)},
285 action_url=urls.messages_link(),
286 )
289def _render_donation__received(
290 data: notification_data_pb2.DonationReceived, loc_context: LocalizationContext
291) -> PushNotificationContent:
292 return _get_content(
293 NotificationTopicAction.donation__received,
294 loc_context,
295 # Other currencies are not yet supported
296 substitutions={"amount_with_currency": f"${data.amount}"},
297 action_url=data.receipt_url,
298 )
301def _render_discussion__create(
302 data: notification_data_pb2.DiscussionCreate, loc_context: LocalizationContext
303) -> PushNotificationContent:
304 return _get_content(
305 NotificationTopicAction.discussion__create,
306 loc_context,
307 substitutions={
308 "title": data.discussion.title,
309 "user": data.author.name,
310 "group_or_community": data.discussion.owner_title,
311 },
312 icon_user=data.author,
313 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug),
314 )
317def _render_discussion__comment(
318 data: notification_data_pb2.DiscussionComment, loc_context: LocalizationContext
319) -> PushNotificationContent:
320 return _get_content(
321 NotificationTopicAction.discussion__comment,
322 loc_context,
323 ios_title=data.author.name,
324 ios_subtitle=data.discussion.title,
325 body=data.reply.content,
326 substitutions={"user": data.author.name, "title": data.discussion.title},
327 icon_user=data.author,
328 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug),
329 )
332def _render_email_address__change(
333 data: notification_data_pb2.EmailAddressChange, loc_context: LocalizationContext
334) -> PushNotificationContent:
335 return _get_content(
336 NotificationTopicAction.email_address__change,
337 loc_context,
338 substitutions={"email": data.new_email},
339 action_url=urls.account_settings_link(),
340 )
343def _render_email_address__verify(loc_context: LocalizationContext) -> PushNotificationContent:
344 return _get_content(
345 NotificationTopicAction.email_address__verify,
346 loc_context,
347 action_url=urls.account_settings_link(),
348 )
351def _render_event__create_any(
352 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext
353) -> PushNotificationContent:
354 return _get_content(
355 NotificationTopicAction.event__create_any,
356 loc_context,
357 substitutions={
358 "title": data.event.title,
359 "user": data.inviting_user.name,
360 "date_and_time": loc_context.localize_datetime(data.event.start_time),
361 },
362 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
363 )
366def _render_event__create_approved(
367 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext
368) -> PushNotificationContent:
369 return _get_content(
370 NotificationTopicAction.event__create_approved,
371 loc_context,
372 substitutions={
373 "title": data.event.title,
374 "user": data.inviting_user.name,
375 "date_and_time": loc_context.localize_datetime(data.event.start_time),
376 },
377 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
378 )
381def _render_event__update(
382 data: notification_data_pb2.EventUpdate, loc_context: LocalizationContext
383) -> PushNotificationContent:
384 # updated_items can include: title, content, start_time, end_time, location,
385 # but a list like that is tricky to localize.
386 return _get_content(
387 NotificationTopicAction.event__update,
388 loc_context,
389 substitutions={
390 "title": data.event.title,
391 "user": data.updating_user.name,
392 },
393 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
394 )
397def _render_event__invite_organizer(
398 data: notification_data_pb2.EventInviteOrganizer, loc_context: LocalizationContext
399) -> PushNotificationContent:
400 return _get_content(
401 NotificationTopicAction.event__invite_organizer,
402 loc_context,
403 substitutions={
404 "title": data.event.title,
405 "user": data.inviting_user.name,
406 },
407 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
408 )
411def _render_event__comment(
412 data: notification_data_pb2.EventComment, loc_context: LocalizationContext
413) -> PushNotificationContent:
414 return _get_content(
415 NotificationTopicAction.event__comment,
416 loc_context,
417 substitutions={
418 "title": data.event.title,
419 "user": data.author.name,
420 },
421 body=data.reply.content,
422 icon_user=data.author,
423 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
424 )
427def _render_event__reminder(
428 data: notification_data_pb2.EventReminder, loc_context: LocalizationContext
429) -> PushNotificationContent:
430 return _get_content(
431 NotificationTopicAction.event__reminder,
432 loc_context,
433 substitutions={
434 "title": data.event.title,
435 "date_and_time": loc_context.localize_datetime(data.event.start_time),
436 },
437 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
438 )
441def _render_event__cancel(
442 data: notification_data_pb2.EventCancel, loc_context: LocalizationContext
443) -> PushNotificationContent:
444 return _get_content(
445 NotificationTopicAction.event__cancel,
446 loc_context,
447 substitutions={
448 "title": data.event.title,
449 "user": data.cancelling_user.name,
450 },
451 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
452 )
455def _render_event__delete(
456 data: notification_data_pb2.EventDelete, loc_context: LocalizationContext
457) -> PushNotificationContent:
458 return _get_content(NotificationTopicAction.event__delete, loc_context, substitutions={"title": data.event.title})
461def _render_friend_request__create(
462 data: notification_data_pb2.FriendRequestCreate, loc_context: LocalizationContext
463) -> PushNotificationContent:
464 return _get_content(
465 NotificationTopicAction.friend_request__create,
466 loc_context,
467 substitutions={"from_user": data.other_user.name},
468 icon_user=data.other_user,
469 action_url=urls.friend_requests_link(),
470 )
473def _render_friend_request__accept(
474 data: notification_data_pb2.FriendRequestAccept, loc_context: LocalizationContext
475) -> PushNotificationContent:
476 return _get_content(
477 NotificationTopicAction.friend_request__accept,
478 loc_context,
479 substitutions={"friend": data.other_user.name},
480 icon_user=data.other_user,
481 action_url=urls.user_link(username=data.other_user.username),
482 )
485def _render_gender__change(
486 data: notification_data_pb2.GenderChange, loc_context: LocalizationContext
487) -> PushNotificationContent:
488 return _get_content(
489 NotificationTopicAction.gender__change,
490 loc_context,
491 substitutions={"gender": data.gender},
492 action_url=urls.account_settings_link(),
493 )
496def _render_general__new_blog_post(
497 data: notification_data_pb2.GeneralNewBlogPost, loc_context: LocalizationContext
498) -> PushNotificationContent:
499 return _get_content(
500 NotificationTopicAction.general__new_blog_post,
501 loc_context,
502 body=data.blurb,
503 substitutions={"title": data.title},
504 action_url=data.url,
505 )
508def _render_host_request__create(
509 data: notification_data_pb2.HostRequestCreate, loc_context: LocalizationContext
510) -> PushNotificationContent:
511 days = (date.fromisoformat(data.host_request.to_date) - date.fromisoformat(data.host_request.from_date)).days + 1
512 return _get_content(
513 NotificationTopicAction.host_request__create,
514 loc_context,
515 substitutions={
516 "user": data.surfer.name,
517 "start_date": loc_context.localize_date_from_iso(data.host_request.from_date),
518 "count": days,
519 },
520 icon_user=data.surfer,
521 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
522 )
525def _render_host_request__message(
526 data: notification_data_pb2.HostRequestMessage, loc_context: LocalizationContext
527) -> PushNotificationContent:
528 # All strings are dynamic, no need to use _get_content
529 return PushNotificationContent(
530 title=data.user.name,
531 ios_title=data.user.name,
532 body=data.text,
533 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
534 icon_url=_avatar_url_or_default(data.user),
535 )
538def _render_host_request__missed_messages(
539 data: notification_data_pb2.HostRequestMissedMessages, loc_context: LocalizationContext
540) -> PushNotificationContent:
541 return _get_content(
542 NotificationTopicAction.host_request__missed_messages,
543 loc_context,
544 substitutions={"user": data.user.name},
545 icon_user=data.user,
546 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
547 )
550def _render_host_request__reminder(
551 data: notification_data_pb2.HostRequestReminder, loc_context: LocalizationContext
552) -> PushNotificationContent:
553 return _get_content(
554 NotificationTopicAction.host_request__reminder,
555 loc_context,
556 substitutions={"user": data.surfer.name},
557 icon_user=data.surfer,
558 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
559 )
562def _render_host_request__accept(
563 data: notification_data_pb2.HostRequestAccept, loc_context: LocalizationContext
564) -> PushNotificationContent:
565 return _get_content(
566 NotificationTopicAction.host_request__accept,
567 loc_context,
568 substitutions={
569 "user": data.host.name,
570 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
571 },
572 icon_user=data.host,
573 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
574 )
577def _render_host_request__reject(
578 data: notification_data_pb2.HostRequestReject, loc_context: LocalizationContext
579) -> PushNotificationContent:
580 return _get_content(
581 NotificationTopicAction.host_request__reject,
582 loc_context,
583 substitutions={
584 "user": data.host.name,
585 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
586 },
587 icon_user=data.host,
588 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
589 )
592def _render_host_request__cancel(
593 data: notification_data_pb2.HostRequestCancel, loc_context: LocalizationContext
594) -> PushNotificationContent:
595 return _get_content(
596 NotificationTopicAction.host_request__cancel,
597 loc_context,
598 substitutions={
599 "user": data.surfer.name,
600 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
601 },
602 icon_user=data.surfer,
603 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
604 )
607def _render_host_request__confirm(
608 data: notification_data_pb2.HostRequestConfirm, loc_context: LocalizationContext
609) -> PushNotificationContent:
610 return _get_content(
611 NotificationTopicAction.host_request__confirm,
612 loc_context,
613 substitutions={
614 "user": data.surfer.name,
615 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
616 },
617 icon_user=data.surfer,
618 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
619 )
622def _render_modnote__create(loc_context: LocalizationContext) -> PushNotificationContent:
623 return _get_content(NotificationTopicAction.modnote__create, loc_context)
626def _render_onboarding__reminder(key: str, loc_context: LocalizationContext) -> PushNotificationContent:
627 string_group = NotificationTopicAction.onboarding__reminder.display.replace(":", "__")
628 string_group += "."
629 string_group += "first" if key == "1" else "subsequent"
630 return _get_content(
631 string_group,
632 loc_context,
633 action_url=urls.edit_profile_link(),
634 )
637def _render_password__change(loc_context: LocalizationContext) -> PushNotificationContent:
638 return _get_content(NotificationTopicAction.password__change, loc_context, action_url=urls.account_settings_link())
641def _render_password_reset__start(
642 data: notification_data_pb2.PasswordResetStart, loc_context: LocalizationContext
643) -> PushNotificationContent:
644 return _get_content(
645 NotificationTopicAction.password_reset__start,
646 loc_context,
647 action_url=urls.account_settings_link(),
648 )
651def _render_password_reset__complete(loc_context: LocalizationContext) -> PushNotificationContent:
652 return _get_content(
653 NotificationTopicAction.password_reset__complete,
654 loc_context,
655 action_url=urls.account_settings_link(),
656 )
659def _render_phone_number__change(
660 data: notification_data_pb2.PhoneNumberChange, loc_context: LocalizationContext
661) -> PushNotificationContent:
662 return _get_content(
663 NotificationTopicAction.phone_number__change,
664 loc_context,
665 substitutions={"phone_number": format_phone_number(data.phone)},
666 action_url=urls.account_settings_link(),
667 )
670def _render_phone_number__verify(
671 data: notification_data_pb2.PhoneNumberVerify, loc_context: LocalizationContext
672) -> PushNotificationContent:
673 return _get_content(
674 NotificationTopicAction.phone_number__verify,
675 loc_context,
676 substitutions={"phone_number": format_phone_number(data.phone)},
677 action_url=urls.account_settings_link(),
678 )
681def _render_postal_verification__postcard_sent(
682 data: notification_data_pb2.PostalVerificationPostcardSent, loc_context: LocalizationContext
683) -> PushNotificationContent:
684 return _get_content(
685 NotificationTopicAction.postal_verification__postcard_sent,
686 loc_context,
687 substitutions={"city": data.city, "country": data.country},
688 action_url=urls.account_settings_link(),
689 )
692def _render_postal_verification__success(loc_context: LocalizationContext) -> PushNotificationContent:
693 return _get_content(
694 NotificationTopicAction.postal_verification__success,
695 loc_context,
696 action_url=urls.account_settings_link(),
697 )
700def _render_postal_verification__failed(
701 data: notification_data_pb2.PostalVerificationFailed, loc_context: LocalizationContext
702) -> PushNotificationContent:
703 body_key: str
704 match data.reason:
705 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_CODE_EXPIRED:
706 body_key = "body_code_expired"
707 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_TOO_MANY_ATTEMPTS:
708 body_key = "body_too_many_attempts"
709 case _:
710 body_key = "body_generic"
712 return _get_content(
713 NotificationTopicAction.postal_verification__failed,
714 loc_context,
715 body=_get_string(NotificationTopicAction.postal_verification__failed, body_key, loc_context),
716 action_url=urls.account_settings_link(),
717 )
720def _render_reference__receive_friend(
721 data: notification_data_pb2.ReferenceReceiveFriend, loc_context: LocalizationContext
722) -> PushNotificationContent:
723 return _get_content(
724 NotificationTopicAction.reference__receive_friend,
725 loc_context,
726 body=data.text,
727 substitutions={"user": data.from_user.name},
728 icon_user=data.from_user,
729 action_url=urls.profile_references_link(),
730 )
733def _render_reference__receive(
734 data: notification_data_pb2.ReferenceReceiveHostRequest, leave_reference_type: str, loc_context: LocalizationContext
735) -> PushNotificationContent:
736 body: str
737 if data.text: 737 ↛ 738line 737 didn't jump to line 738 because the condition on line 737 was never true
738 body = data.text
739 action_url = urls.profile_references_link()
740 else:
741 body = _get_string(
742 "reference__receive",
743 "body_must_write_yours",
744 loc_context,
745 substitutions={"user": data.from_user.name},
746 )
747 action_url = urls.leave_reference_link(
748 reference_type=leave_reference_type,
749 to_user_id=data.from_user.user_id,
750 host_request_id=str(data.host_request_id),
751 )
752 return _get_content(
753 string_group="reference__receive",
754 loc_context=loc_context,
755 body=body,
756 substitutions={"user": data.from_user.name},
757 icon_user=data.from_user,
758 action_url=action_url,
759 )
762def _render_reference__receive_hosted(
763 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext
764) -> PushNotificationContent:
765 # Receiving a hosted reminder means I need to leave a surfed reference
766 return _render_reference__receive(data, leave_reference_type="surfed", loc_context=loc_context)
769def _render_reference__receive_surfed(
770 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext
771) -> PushNotificationContent:
772 return _render_reference__receive(data, leave_reference_type="hosted", loc_context=loc_context)
775def _render_reference__reminder(
776 data: notification_data_pb2.ReferenceReminder, leave_reference_type: str, loc_context: LocalizationContext
777) -> PushNotificationContent:
778 leave_reference_link = urls.leave_reference_link(
779 reference_type=leave_reference_type,
780 to_user_id=data.other_user.user_id,
781 host_request_id=str(data.host_request_id),
782 )
783 return _get_content(
784 string_group="reference__reminder",
785 loc_context=loc_context,
786 substitutions={"count": data.days_left, "user": data.other_user.name},
787 icon_user=data.other_user,
788 action_url=leave_reference_link,
789 )
792def _render_reference__reminder_surfed(
793 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext
794) -> PushNotificationContent:
795 # Surfed reminder means I need to leave a surfed reference
796 return _render_reference__reminder(data, leave_reference_type="surfed", loc_context=loc_context)
799def _render_reference__reminder_hosted(
800 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext
801) -> PushNotificationContent:
802 return _render_reference__reminder(data, leave_reference_type="hosted", loc_context=loc_context)
805def _render_thread__reply(
806 data: notification_data_pb2.ThreadReply, loc_context: LocalizationContext
807) -> PushNotificationContent:
808 parent_title: str
809 view_link: str
810 match data.WhichOneof("reply_parent"):
811 case "event":
812 parent_title = data.event.title
813 view_link = urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug)
814 case "discussion": 814 ↛ 817line 814 didn't jump to line 817 because the pattern on line 814 always matched
815 parent_title = data.discussion.title
816 view_link = urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug)
817 case _:
818 raise Exception("Can only do replies to events and discussions")
820 return _get_content(
821 NotificationTopicAction.thread__reply,
822 loc_context=loc_context,
823 body=data.reply.content,
824 substitutions={"user": data.author.name, "title": parent_title},
825 icon_user=data.author,
826 action_url=view_link,
827 )
830def _render_verification__sv_success(loc_context: LocalizationContext) -> PushNotificationContent:
831 return _get_content(
832 NotificationTopicAction.verification__sv_success,
833 loc_context,
834 action_url=urls.account_settings_link(),
835 )
838def _render_verification__sv_fail(
839 data: notification_data_pb2.VerificationSVFail, loc_context: LocalizationContext
840) -> PushNotificationContent:
841 body_key: str
842 match data.reason:
843 case notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER: 843 ↛ 844line 843 didn't jump to line 844 because the pattern on line 843 never matched
844 body_key = "body_wrong_birthdate_gender"
845 case notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT:
846 body_key = "body_not_a_passport"
847 case notification_data_pb2.SV_FAIL_REASON_DUPLICATE: 847 ↛ 849line 847 didn't jump to line 849 because the pattern on line 847 always matched
848 body_key = "body_duplicate"
849 case _:
850 raise Exception("Shouldn't get here")
852 return _get_content(
853 NotificationTopicAction.verification__sv_fail,
854 loc_context,
855 body=_get_string(NotificationTopicAction.verification__sv_fail, body_key, loc_context),
856 action_url=urls.account_settings_link(),
857 )