Coverage for app / backend / src / couchers / notifications / render_push.py: 87%
289 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 09:44 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 09:44 +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}.push", 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 full_key = f"{string_group.topic}.{string_group.action}.push.{key}"
194 else:
195 full_key = f"{string_group}.{key}"
196 return get_notifs_i18next().localize(full_key, loc_context.locale, substitutions)
199def _avatar_url_or_default(user: api_pb2.User) -> str:
200 return user.avatar_thumbnail_url or urls.icon_url()
203def _render_account_deletion__start(
204 data: notification_data_pb2.AccountDeletionStart, loc_context: LocalizationContext
205) -> PushNotificationContent:
206 return _get_content(NotificationTopicAction.account_deletion__start, loc_context)
209def _render_account_deletion__complete(
210 data: notification_data_pb2.AccountDeletionComplete, loc_context: LocalizationContext
211) -> PushNotificationContent:
212 return _get_content(
213 NotificationTopicAction.account_deletion__complete, loc_context, substitutions={"count": data.undelete_days}
214 )
217def _render_account_deletion__recovered(loc_context: LocalizationContext) -> PushNotificationContent:
218 return _get_content(NotificationTopicAction.account_deletion__recovered, loc_context)
221def _render_activeness__probe(
222 data: notification_data_pb2.ActivenessProbe, loc_context: LocalizationContext
223) -> PushNotificationContent:
224 return _get_content(NotificationTopicAction.activeness__probe, loc_context)
227def _render_api_key__create(
228 data: notification_data_pb2.ApiKeyCreate, loc_context: LocalizationContext
229) -> PushNotificationContent:
230 return _get_content(NotificationTopicAction.api_key__create, loc_context)
233def _render_badge__add(
234 data: notification_data_pb2.BadgeAdd, loc_context: LocalizationContext
235) -> PushNotificationContent:
236 return _get_content(
237 NotificationTopicAction.badge__add,
238 loc_context,
239 substitutions={"badge_name": data.badge_name},
240 action_url=urls.profile_link(),
241 )
244def _render_badge__remove(
245 data: notification_data_pb2.BadgeRemove, loc_context: LocalizationContext
246) -> PushNotificationContent:
247 return _get_content(
248 NotificationTopicAction.badge__remove,
249 loc_context,
250 substitutions={"badge_name": data.badge_name},
251 action_url=urls.profile_link(),
252 )
255def _render_birthdate__change(
256 data: notification_data_pb2.BirthdateChange, loc_context: LocalizationContext
257) -> PushNotificationContent:
258 return _get_content(
259 NotificationTopicAction.birthdate__change,
260 loc_context,
261 substitutions={"birthdate": loc_context.localize_date_from_iso(data.birthdate)},
262 action_url=urls.account_settings_link(),
263 )
266def _render_chat__message(
267 data: notification_data_pb2.ChatMessage, loc_context: LocalizationContext
268) -> PushNotificationContent:
269 # All strings are dynamic, no need to use _get_content
270 return PushNotificationContent(
271 title=data.author.name,
272 ios_title=data.author.name,
273 body=data.text,
274 icon_url=_avatar_url_or_default(data.author),
275 action_url=urls.chat_link(chat_id=data.group_chat_id),
276 )
279def _render_chat__missed_messages(
280 data: notification_data_pb2.ChatMissedMessages, loc_context: LocalizationContext
281) -> PushNotificationContent:
282 return _get_content(
283 NotificationTopicAction.chat__missed_messages,
284 loc_context,
285 substitutions={"count": len(data.messages)},
286 action_url=urls.messages_link(),
287 )
290def _render_donation__received(
291 data: notification_data_pb2.DonationReceived, loc_context: LocalizationContext
292) -> PushNotificationContent:
293 return _get_content(
294 NotificationTopicAction.donation__received,
295 loc_context,
296 # Other currencies are not yet supported
297 substitutions={"amount_with_currency": f"${data.amount}"},
298 action_url=data.receipt_url,
299 )
302def _render_discussion__create(
303 data: notification_data_pb2.DiscussionCreate, loc_context: LocalizationContext
304) -> PushNotificationContent:
305 return _get_content(
306 NotificationTopicAction.discussion__create,
307 loc_context,
308 substitutions={
309 "title": data.discussion.title,
310 "user": data.author.name,
311 "group_or_community": data.discussion.owner_title,
312 },
313 icon_user=data.author,
314 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug),
315 )
318def _render_discussion__comment(
319 data: notification_data_pb2.DiscussionComment, loc_context: LocalizationContext
320) -> PushNotificationContent:
321 return _get_content(
322 NotificationTopicAction.discussion__comment,
323 loc_context,
324 ios_title=data.author.name,
325 ios_subtitle=data.discussion.title,
326 body=data.reply.content,
327 substitutions={"user": data.author.name, "title": data.discussion.title},
328 icon_user=data.author,
329 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug),
330 )
333def _render_email_address__change(
334 data: notification_data_pb2.EmailAddressChange, loc_context: LocalizationContext
335) -> PushNotificationContent:
336 return _get_content(
337 NotificationTopicAction.email_address__change,
338 loc_context,
339 substitutions={"email": data.new_email},
340 action_url=urls.account_settings_link(),
341 )
344def _render_email_address__verify(loc_context: LocalizationContext) -> PushNotificationContent:
345 return _get_content(
346 NotificationTopicAction.email_address__verify,
347 loc_context,
348 action_url=urls.account_settings_link(),
349 )
352def _render_event__create_any(
353 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext
354) -> PushNotificationContent:
355 return _get_content(
356 NotificationTopicAction.event__create_any,
357 loc_context,
358 substitutions={
359 "title": data.event.title,
360 "user": data.inviting_user.name,
361 "date_and_time": loc_context.localize_datetime(data.event.start_time),
362 },
363 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
364 )
367def _render_event__create_approved(
368 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext
369) -> PushNotificationContent:
370 return _get_content(
371 NotificationTopicAction.event__create_approved,
372 loc_context,
373 substitutions={
374 "title": data.event.title,
375 "user": data.inviting_user.name,
376 "date_and_time": loc_context.localize_datetime(data.event.start_time),
377 },
378 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
379 )
382def _render_event__update(
383 data: notification_data_pb2.EventUpdate, loc_context: LocalizationContext
384) -> PushNotificationContent:
385 # updated_items can include: title, content, start_time, end_time, location,
386 # but a list like that is tricky to localize.
387 return _get_content(
388 NotificationTopicAction.event__update,
389 loc_context,
390 substitutions={
391 "title": data.event.title,
392 "user": data.updating_user.name,
393 },
394 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
395 )
398def _render_event__invite_organizer(
399 data: notification_data_pb2.EventInviteOrganizer, loc_context: LocalizationContext
400) -> PushNotificationContent:
401 return _get_content(
402 NotificationTopicAction.event__invite_organizer,
403 loc_context,
404 substitutions={
405 "title": data.event.title,
406 "user": data.inviting_user.name,
407 },
408 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
409 )
412def _render_event__comment(
413 data: notification_data_pb2.EventComment, loc_context: LocalizationContext
414) -> PushNotificationContent:
415 return _get_content(
416 NotificationTopicAction.event__comment,
417 loc_context,
418 substitutions={
419 "title": data.event.title,
420 "user": data.author.name,
421 },
422 body=data.reply.content,
423 icon_user=data.author,
424 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
425 )
428def _render_event__reminder(
429 data: notification_data_pb2.EventReminder, loc_context: LocalizationContext
430) -> PushNotificationContent:
431 return _get_content(
432 NotificationTopicAction.event__reminder,
433 loc_context,
434 substitutions={
435 "title": data.event.title,
436 "date_and_time": loc_context.localize_datetime(data.event.start_time),
437 },
438 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
439 )
442def _render_event__cancel(
443 data: notification_data_pb2.EventCancel, loc_context: LocalizationContext
444) -> PushNotificationContent:
445 return _get_content(
446 NotificationTopicAction.event__cancel,
447 loc_context,
448 substitutions={
449 "title": data.event.title,
450 "user": data.cancelling_user.name,
451 },
452 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug),
453 )
456def _render_event__delete(
457 data: notification_data_pb2.EventDelete, loc_context: LocalizationContext
458) -> PushNotificationContent:
459 return _get_content(NotificationTopicAction.event__delete, loc_context, substitutions={"title": data.event.title})
462def _render_friend_request__create(
463 data: notification_data_pb2.FriendRequestCreate, loc_context: LocalizationContext
464) -> PushNotificationContent:
465 return _get_content(
466 NotificationTopicAction.friend_request__create,
467 loc_context,
468 substitutions={"from_user": data.other_user.name},
469 icon_user=data.other_user,
470 action_url=urls.friend_requests_link(),
471 )
474def _render_friend_request__accept(
475 data: notification_data_pb2.FriendRequestAccept, loc_context: LocalizationContext
476) -> PushNotificationContent:
477 return _get_content(
478 NotificationTopicAction.friend_request__accept,
479 loc_context,
480 substitutions={"friend": data.other_user.name},
481 icon_user=data.other_user,
482 action_url=urls.user_link(username=data.other_user.username),
483 )
486def _render_gender__change(
487 data: notification_data_pb2.GenderChange, loc_context: LocalizationContext
488) -> PushNotificationContent:
489 return _get_content(
490 NotificationTopicAction.gender__change,
491 loc_context,
492 substitutions={"gender": data.gender},
493 action_url=urls.account_settings_link(),
494 )
497def _render_general__new_blog_post(
498 data: notification_data_pb2.GeneralNewBlogPost, loc_context: LocalizationContext
499) -> PushNotificationContent:
500 return _get_content(
501 NotificationTopicAction.general__new_blog_post,
502 loc_context,
503 body=data.blurb,
504 substitutions={"title": data.title},
505 action_url=data.url,
506 )
509def _render_host_request__create(
510 data: notification_data_pb2.HostRequestCreate, loc_context: LocalizationContext
511) -> PushNotificationContent:
512 days = (date.fromisoformat(data.host_request.to_date) - date.fromisoformat(data.host_request.from_date)).days + 1
513 return _get_content(
514 NotificationTopicAction.host_request__create,
515 loc_context,
516 substitutions={
517 "user": data.surfer.name,
518 "start_date": loc_context.localize_date_from_iso(data.host_request.from_date),
519 "count": days,
520 },
521 icon_user=data.surfer,
522 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
523 )
526def _render_host_request__message(
527 data: notification_data_pb2.HostRequestMessage, loc_context: LocalizationContext
528) -> PushNotificationContent:
529 # All strings are dynamic, no need to use _get_content
530 return PushNotificationContent(
531 title=data.user.name,
532 ios_title=data.user.name,
533 body=data.text,
534 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
535 icon_url=_avatar_url_or_default(data.user),
536 )
539def _render_host_request__missed_messages(
540 data: notification_data_pb2.HostRequestMissedMessages, loc_context: LocalizationContext
541) -> PushNotificationContent:
542 return _get_content(
543 NotificationTopicAction.host_request__missed_messages,
544 loc_context,
545 substitutions={"user": data.user.name},
546 icon_user=data.user,
547 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
548 )
551def _render_host_request__reminder(
552 data: notification_data_pb2.HostRequestReminder, loc_context: LocalizationContext
553) -> PushNotificationContent:
554 return _get_content(
555 NotificationTopicAction.host_request__reminder,
556 loc_context,
557 substitutions={"user": data.surfer.name},
558 icon_user=data.surfer,
559 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
560 )
563def _render_host_request__accept(
564 data: notification_data_pb2.HostRequestAccept, loc_context: LocalizationContext
565) -> PushNotificationContent:
566 return _get_content(
567 NotificationTopicAction.host_request__accept,
568 loc_context,
569 substitutions={
570 "user": data.host.name,
571 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
572 },
573 icon_user=data.host,
574 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
575 )
578def _render_host_request__reject(
579 data: notification_data_pb2.HostRequestReject, loc_context: LocalizationContext
580) -> PushNotificationContent:
581 return _get_content(
582 NotificationTopicAction.host_request__reject,
583 loc_context,
584 substitutions={
585 "user": data.host.name,
586 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
587 },
588 icon_user=data.host,
589 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
590 )
593def _render_host_request__cancel(
594 data: notification_data_pb2.HostRequestCancel, loc_context: LocalizationContext
595) -> PushNotificationContent:
596 return _get_content(
597 NotificationTopicAction.host_request__cancel,
598 loc_context,
599 substitutions={
600 "user": data.surfer.name,
601 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
602 },
603 icon_user=data.surfer,
604 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
605 )
608def _render_host_request__confirm(
609 data: notification_data_pb2.HostRequestConfirm, loc_context: LocalizationContext
610) -> PushNotificationContent:
611 return _get_content(
612 NotificationTopicAction.host_request__confirm,
613 loc_context,
614 substitutions={
615 "user": data.surfer.name,
616 "date": loc_context.localize_date_from_iso(data.host_request.from_date),
617 },
618 icon_user=data.surfer,
619 action_url=urls.host_request(host_request_id=data.host_request.host_request_id),
620 )
623def _render_modnote__create(loc_context: LocalizationContext) -> PushNotificationContent:
624 return _get_content(NotificationTopicAction.modnote__create, loc_context)
627def _render_onboarding__reminder(key: str, loc_context: LocalizationContext) -> PushNotificationContent:
628 variant = "first" if key == "1" else "subsequent"
629 return _get_content(
630 f"onboarding.reminder.push.{variant}",
631 loc_context,
632 action_url=urls.edit_profile_link(),
633 )
636def _render_password__change(loc_context: LocalizationContext) -> PushNotificationContent:
637 return _get_content(NotificationTopicAction.password__change, loc_context, action_url=urls.account_settings_link())
640def _render_password_reset__start(
641 data: notification_data_pb2.PasswordResetStart, loc_context: LocalizationContext
642) -> PushNotificationContent:
643 return _get_content(
644 NotificationTopicAction.password_reset__start,
645 loc_context,
646 action_url=urls.account_settings_link(),
647 )
650def _render_password_reset__complete(loc_context: LocalizationContext) -> PushNotificationContent:
651 return _get_content(
652 NotificationTopicAction.password_reset__complete,
653 loc_context,
654 action_url=urls.account_settings_link(),
655 )
658def _render_phone_number__change(
659 data: notification_data_pb2.PhoneNumberChange, loc_context: LocalizationContext
660) -> PushNotificationContent:
661 return _get_content(
662 NotificationTopicAction.phone_number__change,
663 loc_context,
664 substitutions={"phone_number": format_phone_number(data.phone)},
665 action_url=urls.account_settings_link(),
666 )
669def _render_phone_number__verify(
670 data: notification_data_pb2.PhoneNumberVerify, loc_context: LocalizationContext
671) -> PushNotificationContent:
672 return _get_content(
673 NotificationTopicAction.phone_number__verify,
674 loc_context,
675 substitutions={"phone_number": format_phone_number(data.phone)},
676 action_url=urls.account_settings_link(),
677 )
680def _render_postal_verification__postcard_sent(
681 data: notification_data_pb2.PostalVerificationPostcardSent, loc_context: LocalizationContext
682) -> PushNotificationContent:
683 return _get_content(
684 NotificationTopicAction.postal_verification__postcard_sent,
685 loc_context,
686 substitutions={"city": data.city, "country": data.country},
687 action_url=urls.account_settings_link(),
688 )
691def _render_postal_verification__success(loc_context: LocalizationContext) -> PushNotificationContent:
692 return _get_content(
693 NotificationTopicAction.postal_verification__success,
694 loc_context,
695 action_url=urls.account_settings_link(),
696 )
699def _render_postal_verification__failed(
700 data: notification_data_pb2.PostalVerificationFailed, loc_context: LocalizationContext
701) -> PushNotificationContent:
702 body_key: str
703 match data.reason:
704 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_CODE_EXPIRED:
705 body_key = "body_code_expired"
706 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_TOO_MANY_ATTEMPTS:
707 body_key = "body_too_many_attempts"
708 case _:
709 body_key = "body_generic"
711 return _get_content(
712 NotificationTopicAction.postal_verification__failed,
713 loc_context,
714 body=_get_string(NotificationTopicAction.postal_verification__failed, body_key, loc_context),
715 action_url=urls.account_settings_link(),
716 )
719def _render_reference__receive_friend(
720 data: notification_data_pb2.ReferenceReceiveFriend, loc_context: LocalizationContext
721) -> PushNotificationContent:
722 return _get_content(
723 NotificationTopicAction.reference__receive_friend,
724 loc_context,
725 body=data.text,
726 substitutions={"user": data.from_user.name},
727 icon_user=data.from_user,
728 action_url=urls.profile_references_link(),
729 )
732def _render_reference__receive(
733 data: notification_data_pb2.ReferenceReceiveHostRequest, leave_reference_type: str, loc_context: LocalizationContext
734) -> PushNotificationContent:
735 body: str
736 if data.text: 736 ↛ 737line 736 didn't jump to line 737 because the condition on line 736 was never true
737 body = data.text
738 action_url = urls.profile_references_link()
739 else:
740 body = _get_string(
741 "reference._receive_any.push",
742 "body_must_write_yours",
743 loc_context,
744 substitutions={"user": data.from_user.name},
745 )
746 action_url = urls.leave_reference_link(
747 reference_type=leave_reference_type,
748 to_user_id=data.from_user.user_id,
749 host_request_id=str(data.host_request_id),
750 )
751 return _get_content(
752 string_group="reference._receive_any.push",
753 loc_context=loc_context,
754 body=body,
755 substitutions={"user": data.from_user.name},
756 icon_user=data.from_user,
757 action_url=action_url,
758 )
761def _render_reference__receive_hosted(
762 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext
763) -> PushNotificationContent:
764 # Receiving a hosted reminder means I need to leave a surfed reference
765 return _render_reference__receive(data, leave_reference_type="surfed", loc_context=loc_context)
768def _render_reference__receive_surfed(
769 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext
770) -> PushNotificationContent:
771 return _render_reference__receive(data, leave_reference_type="hosted", loc_context=loc_context)
774def _render_reference__reminder(
775 data: notification_data_pb2.ReferenceReminder, leave_reference_type: str, loc_context: LocalizationContext
776) -> PushNotificationContent:
777 leave_reference_link = urls.leave_reference_link(
778 reference_type=leave_reference_type,
779 to_user_id=data.other_user.user_id,
780 host_request_id=str(data.host_request_id),
781 )
782 return _get_content(
783 string_group="reference._reminder_any.push",
784 loc_context=loc_context,
785 substitutions={"count": data.days_left, "user": data.other_user.name},
786 icon_user=data.other_user,
787 action_url=leave_reference_link,
788 )
791def _render_reference__reminder_surfed(
792 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext
793) -> PushNotificationContent:
794 # Surfed reminder means I need to leave a surfed reference
795 return _render_reference__reminder(data, leave_reference_type="surfed", loc_context=loc_context)
798def _render_reference__reminder_hosted(
799 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext
800) -> PushNotificationContent:
801 return _render_reference__reminder(data, leave_reference_type="hosted", loc_context=loc_context)
804def _render_thread__reply(
805 data: notification_data_pb2.ThreadReply, loc_context: LocalizationContext
806) -> PushNotificationContent:
807 parent_title: str
808 view_link: str
809 match data.WhichOneof("reply_parent"):
810 case "event":
811 parent_title = data.event.title
812 view_link = urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug)
813 case "discussion": 813 ↛ 816line 813 didn't jump to line 816 because the pattern on line 813 always matched
814 parent_title = data.discussion.title
815 view_link = urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug)
816 case _:
817 raise Exception("Can only do replies to events and discussions")
819 return _get_content(
820 NotificationTopicAction.thread__reply,
821 loc_context=loc_context,
822 body=data.reply.content,
823 substitutions={"user": data.author.name, "title": parent_title},
824 icon_user=data.author,
825 action_url=view_link,
826 )
829def _render_verification__sv_success(loc_context: LocalizationContext) -> PushNotificationContent:
830 return _get_content(
831 NotificationTopicAction.verification__sv_success,
832 loc_context,
833 action_url=urls.account_settings_link(),
834 )
837def _render_verification__sv_fail(
838 data: notification_data_pb2.VerificationSVFail, loc_context: LocalizationContext
839) -> PushNotificationContent:
840 body_key: str
841 match data.reason:
842 case notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER: 842 ↛ 843line 842 didn't jump to line 843 because the pattern on line 842 never matched
843 body_key = "body_wrong_birthdate_gender"
844 case notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT:
845 body_key = "body_not_a_passport"
846 case notification_data_pb2.SV_FAIL_REASON_DUPLICATE: 846 ↛ 848line 846 didn't jump to line 848 because the pattern on line 846 always matched
847 body_key = "body_duplicate"
848 case _:
849 raise Exception("Shouldn't get here")
851 return _get_content(
852 NotificationTopicAction.verification__sv_fail,
853 loc_context,
854 body=_get_string(NotificationTopicAction.verification__sv_fail, body_key, loc_context),
855 action_url=urls.account_settings_link(),
856 )