Coverage for app / backend / src / couchers / notifications / render_push.py: 86%
296 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
1"""
2Renders a Notification model into a localized push notification.
3"""
5import logging
6from datetime import date
7from functools import lru_cache
8from pathlib import Path
9from typing import Any, assert_never
11from couchers import urls
12from couchers.i18n import LocalizationContext
13from couchers.i18n.i18next import I18Next, LocalizationError
14from couchers.i18n.locales import load_locales
15from couchers.i18n.localize import format_phone_number
16from couchers.models import Notification, NotificationTopicAction
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: 67 ↛ 68line 67 didn't jump to line 68 because the pattern on line 67 never matched
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: 75 ↛ 76line 75 didn't jump to line 76 because the pattern on line 75 never matched
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: 99 ↛ 100line 99 didn't jump to line 100 because the pattern on line 99 never matched
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}", 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=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 string_group = string_group.display.replace(":", "__")
196 key = f"push.{string_group}.{key}"
197 return _get_notifs_i18next().localize(key, loc_context.locale, substitutions)
200def _avatar_url_or_default(user: api_pb2.User) -> str:
201 return user.avatar_thumbnail_url or urls.icon_url()
204def _render_account_deletion__start(
205 data: notification_data_pb2.AccountDeletionStart, loc_context: LocalizationContext
206) -> PushNotificationContent:
207 return _get_content(NotificationTopicAction.account_deletion__start, loc_context)
210def _render_account_deletion__complete(
211 data: notification_data_pb2.AccountDeletionComplete, loc_context: LocalizationContext
212) -> PushNotificationContent:
213 return _get_content(
214 NotificationTopicAction.account_deletion__complete, loc_context, substitutions={"count": data.undelete_days}
215 )
218def _render_account_deletion__recovered(loc_context: LocalizationContext) -> PushNotificationContent:
219 return _get_content(NotificationTopicAction.account_deletion__recovered, loc_context)
222def _render_activeness__probe(
223 data: notification_data_pb2.ActivenessProbe, loc_context: LocalizationContext
224) -> PushNotificationContent:
225 return _get_content(NotificationTopicAction.activeness__probe, loc_context)
228def _render_api_key__create(
229 data: notification_data_pb2.ApiKeyCreate, loc_context: LocalizationContext
230) -> PushNotificationContent:
231 return _get_content(NotificationTopicAction.api_key__create, loc_context)
234def _render_badge__add(
235 data: notification_data_pb2.BadgeAdd, loc_context: LocalizationContext
236) -> PushNotificationContent:
237 return _get_content(
238 NotificationTopicAction.badge__add,
239 loc_context,
240 substitutions={"badge_name": data.badge_name},
241 action_url=urls.profile_link(),
242 )
245def _render_badge__remove(
246 data: notification_data_pb2.BadgeRemove, loc_context: LocalizationContext
247) -> PushNotificationContent:
248 return _get_content(
249 NotificationTopicAction.badge__remove,
250 loc_context,
251 substitutions={"badge_name": data.badge_name},
252 action_url=urls.profile_link(),
253 )
256def _render_birthdate__change(
257 data: notification_data_pb2.BirthdateChange, loc_context: LocalizationContext
258) -> PushNotificationContent:
259 return _get_content(
260 NotificationTopicAction.birthdate__change,
261 loc_context,
262 substitutions={"birthdate": loc_context.localize_date_from_iso(data.birthdate)},
263 action_url=urls.account_settings_link(),
264 )
267def _render_chat__message(
268 data: notification_data_pb2.ChatMessage, loc_context: LocalizationContext
269) -> PushNotificationContent:
270 # All strings are dynamic, no need to use _get_content
271 return PushNotificationContent(
272 title=data.author.name,
273 ios_title=data.author.name,
274 body=data.text,
275 icon_url=_avatar_url_or_default(data.author),
276 action_url=urls.chat_link(chat_id=data.group_chat_id),
277 )
280def _render_chat__missed_messages(
281 data: notification_data_pb2.ChatMissedMessages, loc_context: LocalizationContext
282) -> PushNotificationContent:
283 return _get_content(
284 NotificationTopicAction.chat__missed_messages,
285 loc_context,
286 substitutions={"count": len(data.messages)},
287 action_url=urls.messages_link(),
288 )
291def _render_donation__received(
292 data: notification_data_pb2.DonationReceived, loc_context: LocalizationContext
293) -> PushNotificationContent:
294 return _get_content(
295 NotificationTopicAction.donation__received,
296 loc_context,
297 # Other currencies are not yet supported
298 substitutions={"amount_with_currency": f"${data.amount}"},
299 action_url=data.receipt_url,
300 )
303def _render_discussion__create(
304 data: notification_data_pb2.DiscussionCreate, loc_context: LocalizationContext
305) -> PushNotificationContent:
306 return _get_content(
307 NotificationTopicAction.discussion__create,
308 loc_context,
309 substitutions={
310 "title": data.discussion.title,
311 "user": data.author.name,
312 "group_or_community": data.discussion.owner_title,
313 },
314 icon_user=data.author,
315 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug),
316 )
319def _render_discussion__comment(
320 data: notification_data_pb2.DiscussionComment, loc_context: LocalizationContext
321) -> PushNotificationContent:
322 return _get_content(
323 NotificationTopicAction.discussion__comment,
324 loc_context,
325 ios_title=data.author.name,
326 ios_subtitle=data.discussion.title,
327 body=data.reply.content,
328 substitutions={"user": data.author.name, "title": data.discussion.title},
329 icon_user=data.author,
330 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug),
331 )
334def _render_email_address__change(
335 data: notification_data_pb2.EmailAddressChange, loc_context: LocalizationContext
336) -> PushNotificationContent:
337 return _get_content(
338 NotificationTopicAction.email_address__change,
339 loc_context,
340 substitutions={"email": data.new_email},
341 action_url=urls.account_settings_link(),
342 )
345def _render_email_address__verify(loc_context: LocalizationContext) -> PushNotificationContent:
346 return _get_content(
347 NotificationTopicAction.email_address__verify,
348 loc_context,
349 action_url=urls.account_settings_link(),
350 )
353def _render_event__create_any(
354 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext
355) -> PushNotificationContent:
356 return _get_content(
357 NotificationTopicAction.event__create_any,
358 loc_context,
359 substitutions={
360 "title": data.event.title,
361 "user": data.inviting_user.name,
362 "date_and_time": loc_context.localize_datetime(data.event.start_time),
363 },
364 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
365 )
368def _render_event__create_approved(
369 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext
370) -> PushNotificationContent:
371 return _get_content(
372 NotificationTopicAction.event__create_approved,
373 loc_context,
374 substitutions={
375 "title": data.event.title,
376 "user": data.inviting_user.name,
377 "date_and_time": loc_context.localize_datetime(data.event.start_time),
378 },
379 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
380 )
383def _render_event__update(
384 data: notification_data_pb2.EventUpdate, loc_context: LocalizationContext
385) -> PushNotificationContent:
386 # updated_items can include: title, content, start_time, end_time, location,
387 # but a list like that is tricky to localize.
388 return _get_content(
389 NotificationTopicAction.event__update,
390 loc_context,
391 substitutions={
392 "title": data.event.title,
393 "user": data.updating_user.name,
394 },
395 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
396 )
399def _render_event__invite_organizer(
400 data: notification_data_pb2.EventInviteOrganizer, loc_context: LocalizationContext
401) -> PushNotificationContent:
402 return _get_content(
403 NotificationTopicAction.event__invite_organizer,
404 loc_context,
405 substitutions={
406 "title": data.event.title,
407 "user": data.inviting_user.name,
408 },
409 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
410 )
413def _render_event__comment(
414 data: notification_data_pb2.EventComment, loc_context: LocalizationContext
415) -> PushNotificationContent:
416 return _get_content(
417 NotificationTopicAction.event__comment,
418 loc_context,
419 substitutions={
420 "title": data.event.title,
421 "user": data.author.name,
422 },
423 body=data.reply.content,
424 icon_user=data.author,
425 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
426 )
429def _render_event__reminder(
430 data: notification_data_pb2.EventReminder, loc_context: LocalizationContext
431) -> PushNotificationContent:
432 return _get_content(
433 NotificationTopicAction.event__reminder,
434 loc_context,
435 substitutions={
436 "title": data.event.title,
437 "date_and_time": loc_context.localize_datetime(data.event.start_time),
438 },
439 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
440 )
443def _render_event__cancel(
444 data: notification_data_pb2.EventCancel, loc_context: LocalizationContext
445) -> PushNotificationContent:
446 return _get_content(
447 NotificationTopicAction.event__cancel,
448 loc_context,
449 substitutions={
450 "title": data.event.title,
451 "user": data.cancelling_user.name,
452 },
453 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
454 )
457def _render_event__delete(
458 data: notification_data_pb2.EventDelete, loc_context: LocalizationContext
459) -> PushNotificationContent:
460 return _get_content(NotificationTopicAction.event__delete, loc_context, substitutions={"title": data.event.title})
463def _render_friend_request__create(
464 data: notification_data_pb2.FriendRequestCreate, loc_context: LocalizationContext
465) -> PushNotificationContent:
466 return _get_content(
467 NotificationTopicAction.friend_request__create,
468 loc_context,
469 substitutions={"from_user": data.other_user.name},
470 icon_user=data.other_user,
471 action_url=urls.friend_requests_link(),
472 )
475def _render_friend_request__accept(
476 data: notification_data_pb2.FriendRequestAccept, loc_context: LocalizationContext
477) -> PushNotificationContent:
478 return _get_content(
479 NotificationTopicAction.friend_request__accept,
480 loc_context,
481 substitutions={"friend": data.other_user.name},
482 icon_user=data.other_user,
483 action_url=urls.user_link(username=data.other_user.username),
484 )
487def _render_gender__change(
488 data: notification_data_pb2.GenderChange, loc_context: LocalizationContext
489) -> PushNotificationContent:
490 return _get_content(
491 NotificationTopicAction.gender__change,
492 loc_context,
493 substitutions={"gender": data.gender},
494 action_url=urls.account_settings_link(),
495 )
498def _render_general__new_blog_post(
499 data: notification_data_pb2.GeneralNewBlogPost, loc_context: LocalizationContext
500) -> PushNotificationContent:
501 return _get_content(
502 NotificationTopicAction.general__new_blog_post,
503 loc_context,
504 body=data.blurb,
505 substitutions={"title": data.title},
506 action_url=data.url,
507 )
510def _render_host_request__create(
511 data: notification_data_pb2.HostRequestCreate, loc_context: LocalizationContext
512) -> PushNotificationContent:
513 days = (date.fromisoformat(data.host_request.to_date) - date.fromisoformat(data.host_request.from_date)).days + 1
514 return _get_content(
515 NotificationTopicAction.host_request__create,
516 loc_context,
517 substitutions={
518 "user": data.surfer.name,
519 "start_date": loc_context.localize_date_from_iso(data.host_request.from_date),
520 "count": days,
521 },
522 icon_user=data.surfer,
523 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
524 )
527def _render_host_request__message(
528 data: notification_data_pb2.HostRequestMessage, loc_context: LocalizationContext
529) -> PushNotificationContent:
530 # All strings are dynamic, no need to use _get_content
531 return PushNotificationContent(
532 title=data.user.name,
533 ios_title=data.user.name,
534 body=data.text,
535 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
536 icon_url=_avatar_url_or_default(data.user),
537 )
540def _render_host_request__missed_messages(
541 data: notification_data_pb2.HostRequestMissedMessages, loc_context: LocalizationContext
542) -> PushNotificationContent:
543 return _get_content(
544 NotificationTopicAction.host_request__missed_messages,
545 loc_context,
546 substitutions={"user": data.user.name},
547 icon_user=data.user,
548 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
549 )
552def _render_host_request__reminder(
553 data: notification_data_pb2.HostRequestReminder, loc_context: LocalizationContext
554) -> PushNotificationContent:
555 return _get_content(
556 NotificationTopicAction.host_request__reminder,
557 loc_context,
558 substitutions={"user": data.surfer.name},
559 icon_user=data.surfer,
560 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
561 )
564def _render_host_request__accept(
565 data: notification_data_pb2.HostRequestAccept, loc_context: LocalizationContext
566) -> PushNotificationContent:
567 return _get_content(
568 NotificationTopicAction.host_request__accept,
569 loc_context,
570 substitutions={
571 "user": data.host.name,
572 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
573 },
574 icon_user=data.host,
575 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
576 )
579def _render_host_request__reject(
580 data: notification_data_pb2.HostRequestReject, loc_context: LocalizationContext
581) -> PushNotificationContent:
582 return _get_content(
583 NotificationTopicAction.host_request__reject,
584 loc_context,
585 substitutions={
586 "user": data.host.name,
587 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
588 },
589 icon_user=data.host,
590 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
591 )
594def _render_host_request__cancel(
595 data: notification_data_pb2.HostRequestCancel, loc_context: LocalizationContext
596) -> PushNotificationContent:
597 return _get_content(
598 NotificationTopicAction.host_request__cancel,
599 loc_context,
600 substitutions={
601 "user": data.surfer.name,
602 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
603 },
604 icon_user=data.surfer,
605 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
606 )
609def _render_host_request__confirm(
610 data: notification_data_pb2.HostRequestConfirm, loc_context: LocalizationContext
611) -> PushNotificationContent:
612 return _get_content(
613 NotificationTopicAction.host_request__confirm,
614 loc_context,
615 substitutions={
616 "user": data.surfer.name,
617 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
618 },
619 icon_user=data.surfer,
620 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
621 )
624def _render_modnote__create(loc_context: LocalizationContext) -> PushNotificationContent:
625 return _get_content(NotificationTopicAction.modnote__create, loc_context)
628def _render_onboarding__reminder(key: str, loc_context: LocalizationContext) -> PushNotificationContent:
629 string_group = NotificationTopicAction.onboarding__reminder.display.replace(":", "__")
630 string_group += "."
631 string_group += "first" if key == "1" else "subsequent"
632 return _get_content(
633 string_group,
634 loc_context,
635 action_url=urls.edit_profile_link(),
636 )
639def _render_password__change(loc_context: LocalizationContext) -> PushNotificationContent:
640 return _get_content(NotificationTopicAction.password__change, loc_context, action_url=urls.account_settings_link())
643def _render_password_reset__start(
644 data: notification_data_pb2.PasswordResetStart, loc_context: LocalizationContext
645) -> PushNotificationContent:
646 return _get_content(
647 NotificationTopicAction.password_reset__start,
648 loc_context,
649 action_url=urls.account_settings_link(),
650 )
653def _render_password_reset__complete(loc_context: LocalizationContext) -> PushNotificationContent:
654 return _get_content(
655 NotificationTopicAction.password_reset__complete,
656 loc_context,
657 action_url=urls.account_settings_link(),
658 )
661def _render_phone_number__change(
662 data: notification_data_pb2.PhoneNumberChange, loc_context: LocalizationContext
663) -> PushNotificationContent:
664 return _get_content(
665 NotificationTopicAction.phone_number__change,
666 loc_context,
667 substitutions={"phone_number": format_phone_number(data.phone)},
668 action_url=urls.account_settings_link(),
669 )
672def _render_phone_number__verify(
673 data: notification_data_pb2.PhoneNumberVerify, loc_context: LocalizationContext
674) -> PushNotificationContent:
675 return _get_content(
676 NotificationTopicAction.phone_number__verify,
677 loc_context,
678 substitutions={"phone_number": format_phone_number(data.phone)},
679 action_url=urls.account_settings_link(),
680 )
683def _render_postal_verification__postcard_sent(
684 data: notification_data_pb2.PostalVerificationPostcardSent, loc_context: LocalizationContext
685) -> PushNotificationContent:
686 return _get_content(
687 NotificationTopicAction.postal_verification__postcard_sent,
688 loc_context,
689 substitutions={"city": data.city, "country": data.country},
690 action_url=urls.account_settings_link(),
691 )
694def _render_postal_verification__success(loc_context: LocalizationContext) -> PushNotificationContent:
695 return _get_content(
696 NotificationTopicAction.postal_verification__success,
697 loc_context,
698 action_url=urls.account_settings_link(),
699 )
702def _render_postal_verification__failed(
703 data: notification_data_pb2.PostalVerificationFailed, loc_context: LocalizationContext
704) -> PushNotificationContent:
705 body_key: str
706 match data.reason:
707 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_CODE_EXPIRED:
708 body_key = "body_code_expired"
709 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_TOO_MANY_ATTEMPTS:
710 body_key = "body_too_many_attempts"
711 case _:
712 body_key = "body_generic"
714 return _get_content(
715 NotificationTopicAction.postal_verification__failed,
716 loc_context,
717 body=_get_string(NotificationTopicAction.postal_verification__failed, body_key, loc_context),
718 action_url=urls.account_settings_link(),
719 )
722def _render_reference__receive_friend(
723 data: notification_data_pb2.ReferenceReceiveFriend, loc_context: LocalizationContext
724) -> PushNotificationContent:
725 return _get_content(
726 NotificationTopicAction.reference__receive_friend,
727 loc_context,
728 body=data.text,
729 substitutions={"user": data.from_user.name},
730 icon_user=data.from_user,
731 action_url=urls.profile_references_link(),
732 )
735def _render_reference__receive(
736 data: notification_data_pb2.ReferenceReceiveHostRequest, leave_reference_type: str, loc_context: LocalizationContext
737) -> PushNotificationContent:
738 body: str
739 if data.text: 739 ↛ 740line 739 didn't jump to line 740 because the condition on line 739 was never true
740 body = data.text
741 action_url = urls.profile_references_link()
742 else:
743 body = _get_string(
744 "reference__receive",
745 "body_must_write_yours",
746 loc_context,
747 substitutions={"user": data.from_user.name},
748 )
749 action_url = urls.leave_reference_link(
750 reference_type=leave_reference_type,
751 to_user_id=data.from_user.user_id,
752 host_request_id=str(data.host_request_id),
753 )
754 return _get_content(
755 string_group="reference__receive",
756 loc_context=loc_context,
757 body=body,
758 substitutions={"user": data.from_user.name},
759 icon_user=data.from_user,
760 action_url=action_url,
761 )
764def _render_reference__receive_hosted(
765 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext
766) -> PushNotificationContent:
767 # Receiving a hosted reminder means I need to leave a surfed reference
768 return _render_reference__receive(data, leave_reference_type="surfed", loc_context=loc_context)
771def _render_reference__receive_surfed(
772 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext
773) -> PushNotificationContent:
774 return _render_reference__receive(data, leave_reference_type="hosted", loc_context=loc_context)
777def _render_reference__reminder(
778 data: notification_data_pb2.ReferenceReminder, leave_reference_type: str, loc_context: LocalizationContext
779) -> PushNotificationContent:
780 leave_reference_link = urls.leave_reference_link(
781 reference_type=leave_reference_type,
782 to_user_id=data.other_user.user_id,
783 host_request_id=str(data.host_request_id),
784 )
785 return _get_content(
786 string_group="reference__reminder",
787 loc_context=loc_context,
788 substitutions={"count": data.days_left, "user": data.other_user.name},
789 icon_user=data.other_user,
790 action_url=leave_reference_link,
791 )
794def _render_reference__reminder_surfed(
795 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext
796) -> PushNotificationContent:
797 # Surfed reminder means I need to leave a surfed reference
798 return _render_reference__reminder(data, leave_reference_type="surfed", loc_context=loc_context)
801def _render_reference__reminder_hosted(
802 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext
803) -> PushNotificationContent:
804 return _render_reference__reminder(data, leave_reference_type="hosted", loc_context=loc_context)
807def _render_thread__reply(
808 data: notification_data_pb2.ThreadReply, loc_context: LocalizationContext
809) -> PushNotificationContent:
810 parent_title: str
811 view_link: str
812 match data.WhichOneof("reply_parent"):
813 case "event":
814 parent_title = data.event.title
815 view_link = urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug)
816 case "discussion": 816 ↛ 819line 816 didn't jump to line 819 because the pattern on line 816 always matched
817 parent_title = data.discussion.title
818 view_link = urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug)
819 case _:
820 raise Exception("Can only do replies to events and discussions")
822 return _get_content(
823 NotificationTopicAction.thread__reply,
824 loc_context=loc_context,
825 body=data.reply.content,
826 substitutions={"user": data.author.name, "title": parent_title},
827 icon_user=data.author,
828 action_url=view_link,
829 )
832def _render_verification__sv_success(loc_context: LocalizationContext) -> PushNotificationContent:
833 return _get_content(
834 NotificationTopicAction.verification__sv_success,
835 loc_context,
836 action_url=urls.account_settings_link(),
837 )
840def _render_verification__sv_fail(
841 data: notification_data_pb2.VerificationSVFail, loc_context: LocalizationContext
842) -> PushNotificationContent:
843 body_key: str
844 match data.reason:
845 case notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER: 845 ↛ 846line 845 didn't jump to line 846 because the pattern on line 845 never matched
846 body_key = "body_wrong_birthdate_gender"
847 case notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT:
848 body_key = "body_not_a_passport"
849 case notification_data_pb2.SV_FAIL_REASON_DUPLICATE: 849 ↛ 851line 849 didn't jump to line 851 because the pattern on line 849 always matched
850 body_key = "body_duplicate"
851 case _:
852 raise Exception("Shouldn't get here")
854 return _get_content(
855 NotificationTopicAction.verification__sv_fail,
856 loc_context,
857 body=_get_string(NotificationTopicAction.verification__sv_fail, body_key, loc_context),
858 action_url=urls.account_settings_link(),
859 )
862@lru_cache(maxsize=1)
863def _get_notifs_i18next() -> I18Next:
864 """Gets the I18Next instance for notifications."""
865 return load_locales(Path(__file__).parent / "locales")