Coverage for src/couchers/notifications/render.py: 86%
269 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-08 16:02 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-08 16:02 +0000
1import logging
2from dataclasses import dataclass
3from typing import Any
5from couchers import urls
6from couchers.i18n.i18n import get_raw_translation_string
7from couchers.models import Notification, User
8from couchers.notifications.quick_links import generate_quick_decline_link, generate_unsub_topic_action
9from couchers.proto import notification_data_pb2
10from couchers.templates.v2 import v2avatar, v2date, v2esc, v2phone, v2timestamp
11from couchers.utils import now, to_aware_datetime
13logger = logging.getLogger(__name__)
16@dataclass(kw_only=True)
17class RenderedNotification:
18 # whether the notification is critical and cannot be turned off
19 is_critical: bool = False
20 # whether this email can be sent to someone who is deleted
21 allow_deleted: bool = False
22 # email subject
23 email_subject: str
24 # shows up when listing emails in many clients
25 email_preview: str
26 # corresponds to .mjml + .txt file in templates/v2
27 email_template_name: str
28 # other template args
29 email_template_args: dict[str, Any]
30 # the link label on the topic_action unsubscribe link
31 email_topic_action_unsubscribe_text: str | None = None
32 # the link label on the topic_key unsubscribe link
33 email_topic_key_unsubscribe_text: str | None = None
34 # url to unsubscribe with one click
35 email_list_unsubscribe_url: str | None = None
36 # push notification title
37 push_title: str
38 # push notification content
39 push_body: str
40 # url to an icon for push notifications
41 push_icon: str
42 # url to where clicking on the notification should take you
43 push_url: str
46def render_notification(user: User, notification: Notification) -> RenderedNotification:
47 def get_localized_string(component: str, message_id: str, *, substitutions: dict[str, str] | None = None) -> str:
48 return get_raw_translation_string(
49 user.ui_language_preference, component, message_id, substitutions=substitutions
50 )
52 data = notification.topic_action.data_type.FromString(notification.data) # type: ignore[attr-defined]
53 if notification.topic == "host_request":
54 view_link = urls.host_request(host_request_id=data.host_request.host_request_id)
55 if notification.action == "missed_messages":
56 their_your = "their" if data.am_host else "your"
57 other = data.user
58 # "declined your host request", or similar
59 message = f"{other.name} sent you message(s) in {their_your} host request"
60 return RenderedNotification(
61 email_subject=message,
62 email_preview=message,
63 email_template_name="host_request__plain",
64 email_template_args={
65 "view_link": view_link,
66 "host_request": data.host_request,
67 "message": message,
68 "other": other,
69 },
70 email_topic_action_unsubscribe_text="missed messages in host requests",
71 push_title=message,
72 push_body="Check the app for more info.",
73 push_icon=v2avatar(other),
74 push_url=view_link,
75 )
76 elif notification.action == "create":
77 other = data.surfer
78 message = f"{other.name} sent you a host request"
79 return RenderedNotification(
80 email_subject=message,
81 email_preview=message,
82 email_template_name="host_request__new",
83 email_template_args={
84 "view_link": view_link,
85 "quick_decline_link": generate_quick_decline_link(data.host_request),
86 "host_request": data.host_request,
87 "message": message,
88 "other": other,
89 "text": data.text,
90 },
91 email_topic_action_unsubscribe_text="new host requests",
92 push_title=f"{message}",
93 push_body=f"Dates: {v2date(data.host_request.from_date, user)} to {v2date(data.host_request.to_date, user)}.\n\n{data.text}",
94 push_icon=v2avatar(other),
95 push_url=view_link,
96 )
97 elif notification.action == "message":
98 other = data.user
99 if data.am_host:
100 message = f"{other.name} sent you a message in their host request"
101 else:
102 message = f"{other.name} sent you a message in your host request"
103 topic_action_unsub_text = "messages in host request"
104 return RenderedNotification(
105 email_subject=message,
106 email_preview=message,
107 email_template_name="host_request__message",
108 email_template_args={
109 "view_link": view_link,
110 "host_request": data.host_request,
111 "message": message,
112 "other": other,
113 "text": data.text,
114 },
115 email_topic_action_unsubscribe_text=topic_action_unsub_text,
116 push_title=f"{message}",
117 push_body=f"Dates: {v2date(data.host_request.from_date, user)} to {v2date(data.host_request.to_date, user)}.\n\n{data.text}",
118 push_icon=v2avatar(other),
119 push_url=view_link,
120 )
121 elif notification.action in ["accept", "reject", "confirm", "cancel"]:
122 if notification.action in ["accept", "reject"]:
123 other = data.host
124 their_your = "your"
125 else:
126 other = data.surfer
127 their_your = "their"
128 actioned = {
129 "accept": "accepted",
130 "reject": "declined",
131 "confirm": "confirmed",
132 "cancel": "cancelled",
133 }[notification.action]
134 # "declined your host request", or similar
135 message = f"{other.name} {actioned} {their_your} host request"
136 return RenderedNotification(
137 email_subject=message,
138 email_preview=message,
139 email_template_name="host_request__plain",
140 email_template_args={
141 "view_link": view_link,
142 "host_request": data.host_request,
143 "message": message,
144 "other": other,
145 },
146 email_topic_action_unsubscribe_text=f"{actioned} host requests",
147 push_title=message,
148 push_body="Check the app for more info.",
149 push_icon=v2avatar(other),
150 push_url=view_link,
151 )
152 elif notification.action == "reminder":
153 message = f"You have a pending host request from {data.surfer.name}!"
154 description = "Please respond to the request!"
155 return RenderedNotification(
156 email_subject=message,
157 email_preview=description,
158 email_template_name="host_request__plain",
159 email_template_args={
160 "view_link": view_link,
161 "host_request": data.host_request,
162 "message": description,
163 "other": data.surfer,
164 },
165 email_topic_action_unsubscribe_text="Pending host request reminders",
166 push_title=message,
167 push_body=description,
168 push_icon=v2avatar(data.surfer),
169 push_url=view_link,
170 )
171 elif notification.topic_action.display == "password:change":
172 title = "Your password was changed"
173 message = "Your login password for Couchers.org was changed."
174 return RenderedNotification(
175 is_critical=True,
176 email_subject=title,
177 email_preview=message,
178 email_template_name="security",
179 email_template_args={
180 "title": title,
181 "message": message,
182 },
183 push_title=title,
184 push_body=message,
185 push_icon=urls.icon_url(),
186 push_url=urls.account_settings_link(),
187 )
188 elif notification.topic_action.display == "password_reset:start":
189 message = "Someone initiated a password change on your account."
190 return RenderedNotification(
191 is_critical=True,
192 email_subject="Reset your Couchers.org password",
193 email_preview=message,
194 email_template_name="password_reset",
195 email_template_args={
196 "password_reset_link": urls.password_reset_link(password_reset_token=data.password_reset_token)
197 },
198 push_title="A password reset was initiated on your account",
199 push_body=message,
200 push_icon=urls.icon_url(),
201 push_url=urls.account_settings_link(),
202 )
203 elif notification.topic_action.display == "password_reset:complete":
204 title = "Your password was successfully reset"
205 message = "Your password on Couchers.org was changed. If that was you, then no further action is needed."
206 return RenderedNotification(
207 is_critical=True,
208 email_subject=title,
209 email_preview=title,
210 email_template_name="security",
211 email_template_args={
212 "title": title,
213 "message": message,
214 },
215 push_title=title,
216 push_body=message,
217 push_icon=urls.icon_url(),
218 push_url=urls.account_settings_link(),
219 )
220 elif notification.topic_action.display == "email_address:change":
221 title = "An email change was initiated on your account"
222 message = f"An email change to the email <b>{data.new_email}</b> was initiated on your account."
223 message_plain = f"An email change to the email {data.new_email} was initiated on your account."
224 return RenderedNotification(
225 is_critical=True,
226 email_subject=title,
227 email_preview=title,
228 email_template_name="security",
229 email_template_args={
230 "title": title,
231 "message": message,
232 },
233 push_title=title,
234 push_body=message_plain,
235 push_icon=urls.icon_url(),
236 push_url=urls.account_settings_link(),
237 )
238 elif notification.topic_action.display == "email_address:verify":
239 title = "Email change completed"
240 message = "Your new email address has been verified."
241 return RenderedNotification(
242 is_critical=True,
243 email_subject=title,
244 email_preview=message,
245 email_template_name="security",
246 email_template_args={
247 "title": title,
248 "message": message,
249 },
250 push_title=title,
251 push_body=message,
252 push_icon=urls.icon_url(),
253 push_url=urls.account_settings_link(),
254 )
255 elif notification.topic_action.display == "phone_number:change":
256 title = "Phone verification started"
257 message = f"You started phone number verification with the number <b>{v2phone(data.phone)}</b>."
258 message_plain = f"You started phone number verification with the number {v2phone(data.phone)}."
259 return RenderedNotification(
260 is_critical=True,
261 email_subject=title,
262 email_preview=message,
263 email_template_name="security",
264 email_template_args={
265 "title": title,
266 "message": message,
267 },
268 push_title=title,
269 push_body=message_plain,
270 push_icon=urls.icon_url(),
271 push_url=urls.feature_preview_link(),
272 )
273 elif notification.topic_action.display == "phone_number:verify":
274 title = "Phone successfully verified"
275 message = f"Your phone was successfully verified as <b>{v2phone(data.phone)}</b> on Couchers.org."
276 message_plain = f"Your phone was successfully verified as {v2phone(data.phone)} on Couchers.org."
277 return RenderedNotification(
278 is_critical=True,
279 email_subject=title,
280 email_preview=message_plain,
281 email_template_name="security",
282 email_template_args={
283 "title": title,
284 "message": message,
285 },
286 push_title=title,
287 push_body=message_plain,
288 push_icon=urls.icon_url(),
289 push_url=urls.feature_preview_link(),
290 )
291 elif notification.topic_action.display == "gender:change":
292 title = "Your gender was changed"
293 message = f"Your gender on Couchers.org was changed to <b>{data.gender}</b> by an admin."
294 message_plain = f"Your gender on Couchers.org was changed to {data.gender} by an admin."
295 return RenderedNotification(
296 is_critical=True,
297 email_subject=title,
298 email_preview=message_plain,
299 email_template_name="security",
300 email_template_args={
301 "title": title,
302 "message": message,
303 },
304 push_title=title,
305 push_body=message_plain,
306 push_icon=urls.icon_url(),
307 push_url=urls.account_settings_link(),
308 )
309 elif notification.topic_action.display == "birthdate:change":
310 title = "Your date of birth was changed"
311 message = (
312 f"Your date of birth on Couchers.org was changed to <b>{v2date(data.birthdate, user)}</b> by an admin."
313 )
314 message_plain = f"Your date of birth on Couchers.org was changed to {v2date(data.birthdate, user)} by an admin."
315 return RenderedNotification(
316 is_critical=True,
317 email_subject=title,
318 email_preview=message_plain,
319 email_template_name="security",
320 email_template_args={
321 "title": title,
322 "message": message,
323 },
324 push_title=title,
325 push_body=message_plain,
326 push_icon=urls.icon_url(),
327 push_url=urls.account_settings_link(),
328 )
329 elif notification.topic_action.display == "api_key:create":
330 return RenderedNotification(
331 is_critical=True,
332 email_subject="Your API key for Couchers.org",
333 email_preview="We have issued you an API key as per your request.",
334 email_template_name="api_key",
335 email_template_args={
336 "api_key": data.api_key,
337 "expiry": data.expiry,
338 },
339 push_title="An API key was created for your account",
340 push_body="Details were sent to you via email.",
341 push_icon=urls.icon_url(),
342 push_url=urls.app_link(),
343 )
344 elif notification.topic_action.display in ["badge:add", "badge:remove"]:
345 actioned = "added to" if notification.action == "add" else "removed from"
346 title = f"The {data.badge_name} badge was {actioned} your profile"
347 return RenderedNotification(
348 email_subject=title,
349 email_preview=title,
350 email_template_name="badge",
351 email_template_args={
352 "badge_name": data.badge_name,
353 "actioned": actioned,
354 "unsub_type": "badge additions" if notification.action == "add" else "badge removals",
355 },
356 email_topic_action_unsubscribe_text="badge additions" if notification.action == "add" else "badge removals",
357 push_title=title,
358 push_body=(
359 "Check out your profile to see the new badge!"
360 if notification.action == "add"
361 else "You can see all your badges on your profile."
362 ),
363 push_icon=urls.icon_url(),
364 push_url=urls.profile_link(),
365 email_list_unsubscribe_url=generate_unsub_topic_action(notification),
366 )
367 elif notification.topic_action.display == "donation:received":
368 title = "Thank you for your donation to Couchers.org!"
369 message = f"Thank you so much for your donation of ${data.amount} to Couchers.org."
370 return RenderedNotification(
371 is_critical=True,
372 email_subject=title,
373 email_preview=message,
374 email_template_name="donation_received",
375 email_template_args={
376 "amount": data.amount,
377 "receipt_url": data.receipt_url,
378 },
379 push_title=title,
380 push_body=message,
381 push_icon=urls.icon_url(),
382 push_url=data.receipt_url,
383 )
384 elif notification.topic_action.display == "friend_request:create":
385 other = data.other_user
386 preview = f"You've received a friend request from {other.name}"
387 return RenderedNotification(
388 email_subject=f"{other.name} wants to be your friend on Couchers.org!",
389 email_preview=preview,
390 email_template_name="friend_request",
391 email_template_args={
392 "friend_requests_link": urls.friend_requests_link(),
393 "other": other,
394 },
395 email_topic_action_unsubscribe_text="new friend requests",
396 push_title=f"{other.name} wants to be your friend",
397 push_body=preview,
398 push_icon=v2avatar(other),
399 push_url=urls.friend_requests_link(),
400 )
401 elif notification.topic_action.display == "friend_request:accept":
402 other = data.other_user
403 title = f"{other.name} accepted your friend request!"
404 preview = f"{v2esc(other.name)} has accepted your friend request"
405 return RenderedNotification(
406 email_subject=title,
407 email_preview=preview,
408 email_template_name="friend_request_accepted",
409 email_template_args={
410 "other_user_link": urls.user_link(username=other.username),
411 "other": other,
412 },
413 email_topic_action_unsubscribe_text="accepted friend requests",
414 push_title=title,
415 push_body=preview,
416 push_icon=v2avatar(other),
417 push_url=urls.user_link(username=other.username),
418 )
419 elif notification.topic_action.display == "account_deletion:start":
420 return RenderedNotification(
421 is_critical=True,
422 allow_deleted=True,
423 email_subject="Confirm your Couchers.org account deletion",
424 email_preview="Please confirm that you want to delete your Couchers.org account.",
425 email_template_name="account_deletion_start",
426 email_template_args={
427 "deletion_link": urls.delete_account_link(account_deletion_token=data.deletion_token),
428 },
429 push_title="Account deletion initiated",
430 push_body="Someone initiated the deletion of your Couchers.org account. To delete your account, please follow the link in the email we sent you.",
431 push_icon=urls.icon_url(),
432 push_url=urls.app_link(),
433 )
434 elif notification.topic_action.display == "account_deletion:complete":
435 title = "Your Couchers.org account has been deleted"
436 return RenderedNotification(
437 is_critical=True,
438 allow_deleted=True,
439 email_subject=title,
440 email_preview="We have deleted your Couchers.org account, to undo, follow the link in this email.",
441 email_template_name="account_deletion_complete",
442 email_template_args={
443 "undelete_link": urls.recover_account_link(account_undelete_token=data.undelete_token),
444 "days": data.undelete_days,
445 },
446 push_title=title,
447 push_body=f"You can still undo this by following the link we emailed to you within {data.undelete_days} days.",
448 push_icon=urls.icon_url(),
449 push_url=urls.app_link(),
450 )
451 elif notification.topic_action.display == "account_deletion:recovered":
452 title = "Your Couchers.org account has been recovered!"
453 subtitle = "We have recovered your Couchers.org account as per your request! Welcome back!"
454 return RenderedNotification(
455 is_critical=True,
456 allow_deleted=True,
457 email_subject=title,
458 email_preview=subtitle,
459 email_template_name="account_deletion_recovered",
460 email_template_args={
461 "app_link": urls.app_link(),
462 },
463 push_title=title,
464 push_body=subtitle,
465 push_icon=urls.icon_url(),
466 push_url=urls.app_link(),
467 )
468 elif notification.topic_action.display == "chat:message":
469 return RenderedNotification(
470 email_subject=data.message,
471 email_preview="You received a message on Couchers.org!",
472 email_template_name="chat_message",
473 email_template_args={
474 "author": data.author,
475 "message": data.message,
476 "text": data.text,
477 "view_link": urls.chat_link(chat_id=data.group_chat_id),
478 },
479 email_topic_action_unsubscribe_text="new chat messages",
480 email_topic_key_unsubscribe_text="this chat (mute)",
481 push_title=data.message,
482 push_body=data.text,
483 push_icon=v2avatar(data.author),
484 push_url=urls.chat_link(chat_id=data.group_chat_id),
485 )
486 elif notification.topic_action.display == "chat:missed_messages":
487 return RenderedNotification(
488 email_subject="You have unseen messages on Couchers.org!",
489 email_preview="You missed some messages on the platform.",
490 email_template_name="chat_unseen_messages",
491 email_template_args={
492 "items": [
493 {
494 "author": item.author,
495 "message": item.message,
496 "text": item.text,
497 "view_link": urls.chat_link(chat_id=item.group_chat_id),
498 }
499 for item in data.messages
500 ]
501 },
502 email_topic_action_unsubscribe_text="unseen chat messages",
503 push_title="You have unseen messages on Couchers.org",
504 push_body="Please check out any messages you missed.",
505 push_icon=urls.icon_url(),
506 push_url=urls.messages_link(),
507 )
508 elif notification.topic == "event":
509 event = data.event
510 time_display = f"{v2timestamp(event.start_time, user)} - {v2timestamp(event.end_time, user)}"
511 event_link = urls.event_link(occurrence_id=event.event_id, slug=event.slug)
512 if notification.action in ["create_approved", "create_any"]:
513 # create_approved = invitation, approved by mods
514 # create_any = new event created by anyone (no need for approval) -- off by default
515 body = f"{time_display}\n"
516 if notification.action == "create_approved":
517 subject = f'{data.inviting_user.name} invited you to "{event.title}"'
518 start_text = "You've been invited to a new event"
519 body += f"Invited by {data.inviting_user.name}\n\n"
520 elif notification.action == "create_any":
521 subject = f'{data.inviting_user.name} created an event called "{event.title}"'
522 start_text = "A new event was created"
523 body += f"Created by {data.inviting_user.name}\n\n"
524 body += event.content
525 community_link = (
526 urls.community_link(node_id=data.in_community.community_id, slug=data.in_community.slug)
527 if data.in_community
528 else None
529 )
530 return RenderedNotification(
531 email_subject=subject,
532 email_preview=f"{start_text} on Couchers.org!",
533 email_template_name="event_create",
534 email_template_args={
535 "inviting_user": data.inviting_user,
536 "time_display": time_display,
537 "start_text": start_text,
538 "nearby": "nearby" if data.nearby else None,
539 "community": data.in_community if data.in_community else None,
540 "community_link": community_link,
541 "event": event,
542 "view_link": event_link,
543 },
544 email_topic_action_unsubscribe_text=(
545 "new events by community members"
546 if notification.action == "create_any"
547 else "invitations to events (approved by moderators)"
548 ),
549 push_title=subject,
550 push_body=body,
551 push_icon=v2avatar(data.inviting_user),
552 push_url=event_link,
553 )
554 elif notification.action == "update":
555 updated_text = ", ".join(data.updated_items)
556 body = f"{time_display}\n"
557 body += f"{data.updating_user.name} updated: {updated_text}\n\n"
558 body += event.content
559 return RenderedNotification(
560 email_subject=f'{data.updating_user.name} updated "{event.title}"',
561 email_preview="An event you are subscribed to was updated.",
562 email_template_name="event_update",
563 email_template_args={
564 "updating_user": data.updating_user,
565 "time_display": time_display,
566 "event": event,
567 "updated_text": updated_text,
568 "view_link": event_link,
569 },
570 email_topic_action_unsubscribe_text="event updates",
571 push_title=f'{data.updating_user.name} updated "{event.title}"',
572 push_body=body,
573 push_icon=v2avatar(data.updating_user),
574 push_url=event_link,
575 )
576 elif notification.action == "cancel":
577 body = f"{time_display}\n"
578 body += f"The event has been cancelled by {data.cancelling_user.name}.\n\n"
579 body += event.content
580 return RenderedNotification(
581 email_subject=f'{data.cancelling_user.name} cancelled "{event.title}"',
582 email_preview="An event you are subscribed to has been cancelled.",
583 email_template_name="event_cancel",
584 email_template_args={
585 "cancelling_user": data.cancelling_user,
586 "time_display": time_display,
587 "event": event,
588 "view_link": event_link,
589 },
590 email_topic_action_unsubscribe_text="event cancellations",
591 push_title=f'{data.cancelling_user.name} cancelled "{event.title}"',
592 push_body=body,
593 push_icon=v2avatar(data.cancelling_user),
594 push_url=event_link,
595 )
596 elif notification.action == "delete":
597 return RenderedNotification(
598 email_subject=f'A moderator deleted "{event.title}"',
599 email_preview="An event you are subscribed to has been deleted.",
600 email_template_name="event_delete",
601 email_template_args={
602 "time_display": time_display,
603 "event": event,
604 },
605 email_topic_action_unsubscribe_text="event deletions",
606 push_title=f'A moderator deleted "{event.title}"',
607 push_body=f"{time_display}\nThe event has been deleted by the moderators.",
608 push_icon=urls.icon_url(),
609 push_url=urls.app_link(),
610 )
611 elif notification.action == "invite_organizer":
612 body = f"{time_display}\n"
613 body += f"Invited to co-organize by {data.inviting_user.name}\n\n"
614 body += event.content
615 return RenderedNotification(
616 email_subject=f'{data.inviting_user.name} invited you to co-organize "{event.title}"',
617 email_preview="You were invited to co-organize an event on Couchers.org.",
618 email_template_name="event_invite_organizer",
619 email_template_args={
620 "inviting_user": data.inviting_user,
621 "time_display": time_display,
622 "event": event,
623 "view_link": event_link,
624 },
625 email_topic_action_unsubscribe_text="invitations to co-organize events",
626 push_title=f'{data.inviting_user.name} invited you to co-organize "{event.title}"',
627 push_body=body,
628 push_icon=v2avatar(data.inviting_user),
629 push_url=event_link,
630 )
631 elif notification.action == "comment":
632 body = f"{time_display}\n"
633 body += f"{data.author.name} commented:\n\n"
634 body += data.reply.content
635 return RenderedNotification(
636 email_subject=f'{data.author.name} commented on "{event.title}"',
637 email_preview="Someone commented on an event you are attending.",
638 email_template_name="event_comment",
639 email_template_args={
640 "author": data.author,
641 "time_display": time_display,
642 "event": event,
643 "content": data.reply.content,
644 "view_link": event_link,
645 },
646 email_topic_action_unsubscribe_text="event comments",
647 push_title=f'{data.author.name} commented on "{event.title}"',
648 push_body=body,
649 push_icon=v2avatar(data.author),
650 push_url=event_link,
651 )
652 elif notification.action == "reminder":
653 body = "Don't forget your upcoming event on Couchers.org\n"
654 body += f"{time_display}\n"
655 body += data.event.content
656 return RenderedNotification(
657 email_subject=f'Reminder: "{data.event.title}" starts soon',
658 email_preview="Don't forget your upcoming event on Couchers.org",
659 email_template_name="event_reminder",
660 email_template_args={
661 "time_display": time_display,
662 "event": event,
663 "view_link": event_link,
664 },
665 email_topic_action_unsubscribe_text="event reminders",
666 push_title=f'"{data.event.title}" starts soon',
667 push_body=body,
668 push_icon=urls.icon_url(),
669 push_url=event_link,
670 )
671 elif notification.topic == "discussion":
672 discussion = data.discussion
673 discussion_link = urls.discussion_link(discussion_id=discussion.discussion_id, slug=discussion.slug)
674 if notification.action == "create":
675 body = f"{data.author.name} created a discussion in {discussion.owner_title}: {discussion.title}\n\n"
676 body += discussion.content
677 return RenderedNotification(
678 email_subject=f'{data.author.name} created a discussion: "{discussion.title}"',
679 email_preview="Someone created a discussion in a community or group you are subscribed to.",
680 email_template_name="discussion_create",
681 email_template_args={
682 "author": data.author,
683 "discussion": discussion,
684 "view_link": discussion_link,
685 },
686 email_topic_action_unsubscribe_text="new discussions",
687 push_title=discussion.title,
688 push_body=body,
689 push_icon=v2avatar(data.author),
690 push_url=discussion_link,
691 )
692 elif notification.action == "comment":
693 body = f"{data.author.name} commented:\n\n"
694 body += data.reply.content
695 return RenderedNotification(
696 email_subject=f'{data.author.name} commented on "{discussion.title}"',
697 email_preview="Someone commented on your discussion.",
698 email_template_name="discussion_comment",
699 email_template_args={
700 "author": data.author,
701 "discussion": discussion,
702 "reply": data.reply,
703 "view_link": discussion_link,
704 },
705 email_topic_action_unsubscribe_text="discussion comments",
706 push_title=discussion.title,
707 push_body=body,
708 push_icon=v2avatar(data.author),
709 push_url=discussion_link,
710 )
711 elif notification.topic_action.display == "thread:reply":
712 parent = data.WhichOneof("reply_parent")
713 if parent == "event":
714 title = data.event.title
715 view_link = urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug)
716 elif parent == "discussion":
717 title = data.discussion.title
718 view_link = urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug)
719 else:
720 raise Exception("Can only do replies to events and discussions")
722 body = f"{data.author.name} replied:\n\n"
723 body += data.reply.content
724 return RenderedNotification(
725 email_subject=f'{data.author.name} replied in "{title}"',
726 email_preview="Someone replied in a comment thread you have participated in.",
727 email_template_name="comment_reply",
728 email_template_args={
729 "author": data.author,
730 "title": title,
731 "reply": data.reply,
732 "view_link": view_link,
733 },
734 email_topic_action_unsubscribe_text="comment replies",
735 push_title=title,
736 push_body=body,
737 push_icon=v2avatar(data.author),
738 push_url=view_link,
739 )
740 elif notification.topic == "reference":
741 if notification.action == "receive_friend":
742 title = f"You've received a friend reference from {data.from_user.name}!"
743 return RenderedNotification(
744 email_subject=title,
745 email_preview=v2esc(data.text),
746 email_template_name="friend_reference",
747 email_template_args={
748 "from_user": data.from_user,
749 "profile_references_link": urls.profile_references_link(),
750 "text": data.text,
751 },
752 email_topic_action_unsubscribe_text="new references from friends",
753 push_title=title,
754 push_body=data.text,
755 push_icon=v2avatar(data.from_user),
756 push_url=urls.profile_references_link(),
757 )
758 elif notification.action in ["receive_hosted", "receive_surfed"]:
759 title = f"You've received a reference from {data.from_user.name}!"
760 # what was my type? i surfed with them if i received a "hosted" request
761 surfed = notification.action == "receive_hosted"
762 leave_reference_link = urls.leave_reference_link(
763 reference_type="surfed" if surfed else "hosted",
764 to_user_id=data.from_user.user_id,
765 host_request_id=data.host_request_id,
766 )
767 profile_references_link = urls.profile_references_link()
768 if data.text:
769 body = v2esc(data.text)
770 push_url = profile_references_link
771 else:
772 body = "Please go and write a reference for them too. It's a nice gesture and helps us build a community together!"
773 push_url = leave_reference_link
774 return RenderedNotification(
775 email_subject=title,
776 email_preview=body,
777 email_template_name="host_reference",
778 email_template_args={
779 "from_user": data.from_user,
780 "leave_reference_link": leave_reference_link,
781 "profile_references_link": profile_references_link,
782 "text": data.text,
783 "both_written": True if data.text else False,
784 "surfed": surfed,
785 },
786 email_topic_action_unsubscribe_text="new references from " + ("hosts" if surfed else "surfers"),
787 push_title=title,
788 push_body=body,
789 push_icon=v2avatar(data.from_user),
790 push_url=push_url,
791 )
792 elif notification.action in ["reminder_hosted", "reminder_surfed"]:
793 # what was my type? i surfed with them if i get a surfed reminder
794 surfed = notification.action == "reminder_surfed"
795 leave_reference_link = urls.leave_reference_link(
796 reference_type="surfed" if surfed else "hosted",
797 to_user_id=data.other_user.user_id,
798 host_request_id=data.host_request_id,
799 )
800 title = f"You have {data.days_left} days to write a reference for {data.other_user.name}!"
801 preview = "It's a nice gesture to write references and helps us build a community together! References will become visible 2 weeks after the stay, or when you've both written a reference for each other, whichever happens first."
802 return RenderedNotification(
803 email_subject=title,
804 email_preview=preview,
805 email_template_name="reference_reminder",
806 email_template_args={
807 "other_user": data.other_user,
808 "leave_reference_link": leave_reference_link,
809 "days_left": str(data.days_left),
810 "surfed": surfed,
811 },
812 email_topic_action_unsubscribe_text=("surfed" if surfed else "hosted") + " reference reminders",
813 push_title=title,
814 push_body=preview,
815 push_icon=v2avatar(data.other_user),
816 push_url=leave_reference_link,
817 )
818 elif notification.topic_action.display == "onboarding:reminder":
819 if notification.key == "1":
820 return RenderedNotification(
821 email_subject="Welcome to Couchers.org and the future of couch surfing",
822 email_preview="We are so excited to have you join our community!",
823 email_template_name="onboarding1",
824 email_template_args={
825 "app_link": urls.app_link(),
826 "edit_profile_link": urls.edit_profile_link(),
827 },
828 email_topic_action_unsubscribe_text="onboarding emails",
829 push_title="Welcome to Couchers.org and the future of couch surfing",
830 push_body=f"Hi {v2esc(user.name)}! We are excited that you have joined us! Please take a moment to complete your profile with a picture and a bit of text about yourself!",
831 push_icon=urls.icon_url(),
832 push_url=urls.edit_profile_link(),
833 )
834 elif notification.key == "2":
835 return RenderedNotification(
836 email_subject="Complete your profile on Couchers.org",
837 email_preview="We would ask one big favour of you: please fill out your profile by adding a photo and some text.",
838 email_template_name="onboarding2",
839 email_template_args={
840 "edit_profile_link": urls.edit_profile_link(),
841 },
842 email_topic_action_unsubscribe_text="onboarding emails",
843 push_title="Please complete your profile on Couchers.org!",
844 push_body=f"Hi {v2esc(user.name)}! We would ask one big favour of you: please fill out your profile by adding a photo and some text.",
845 push_icon=urls.icon_url(),
846 push_url=urls.edit_profile_link(),
847 )
848 elif notification.topic_action.display == "modnote:create":
849 title = "You have received a mod note"
850 message = "You have received an important note from the moderators. You must read and acknowledge it before continuing to use the platform."
851 return RenderedNotification(
852 is_critical=True,
853 email_subject=title,
854 email_preview=message,
855 email_template_name="mod_note",
856 email_template_args={"title": title},
857 push_title="You received a mod note",
858 push_body="You need to read and acknowledge the note before continuing to use the platform.",
859 push_icon=urls.icon_url(),
860 push_url=urls.app_link(),
861 )
862 elif notification.topic_action.display == "verification:sv_success":
863 title = "Strong Verification succeeded"
864 message = "You have been verified with Strong Verification! You will now see a tick next to your name on the platform."
865 return RenderedNotification(
866 is_critical=True,
867 email_subject=title,
868 email_preview=message,
869 email_template_name="strong_verification_success",
870 email_template_args={
871 "message": message,
872 },
873 push_title=title,
874 push_body=message,
875 push_icon=urls.icon_url(),
876 push_url=urls.account_settings_link(),
877 )
878 elif notification.topic_action.display == "verification:sv_fail":
879 title = "Strong Verification failed"
880 reason_message: str
881 if data.reason == notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER:
882 reason_message = "The date of birth or gender on your profile does not match the date of birth or sex on your passport. Please contact the support team to update your date of birth or gender, or if your passport sex does not match your gender identity."
883 elif data.reason == notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT:
884 reason_message = "You tried to verify with a document that is not a passport. You can only use a passport for Strong Verification."
885 elif data.reason == notification_data_pb2.SV_FAIL_REASON_DUPLICATE:
886 reason_message = "You tried to verify with a passport that has already been used for verification. Please use another passport."
887 else:
888 raise Exception("Shouldn't get here")
889 return RenderedNotification(
890 is_critical=True,
891 email_subject=title,
892 email_preview=title,
893 email_template_name="security",
894 email_template_args={
895 "title": title,
896 "message": reason_message,
897 },
898 push_title=title,
899 push_body=reason_message,
900 push_icon=urls.icon_url(),
901 push_url=urls.account_settings_link(),
902 )
903 elif notification.topic == "postal_verification":
904 if notification.action == "postcard_sent":
905 title = "Your verification postcard is on its way"
906 message = f"We've sent a postcard with your verification code to {data.city}, {data.country}. It should arrive within 1-3 weeks depending on your location. Once it arrives, enter the code on the platform to complete verification."
907 return RenderedNotification(
908 is_critical=True,
909 email_subject=title,
910 email_preview=message,
911 email_template_name="security",
912 email_template_args={
913 "title": title,
914 "message": message,
915 },
916 push_title=title,
917 push_body=f"Postcard sent to {data.city}, {data.country}. Expect it within 1-3 weeks.",
918 push_icon=urls.icon_url(),
919 push_url=urls.account_settings_link(),
920 )
921 elif notification.action == "success":
922 title = "Postal Verification succeeded"
923 message = "You have been verified with Postal Verification! Your address has been confirmed."
924 return RenderedNotification(
925 is_critical=True,
926 email_subject=title,
927 email_preview=message,
928 email_template_name="security",
929 email_template_args={
930 "title": title,
931 "message": message,
932 },
933 push_title=title,
934 push_body=message,
935 push_icon=urls.icon_url(),
936 push_url=urls.account_settings_link(),
937 )
938 elif notification.action == "failed":
939 title = "Postal Verification failed"
940 reason_message: str
941 if data.reason == notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_CODE_EXPIRED:
942 reason_message = "Your verification code has expired. Codes are valid for 90 days after the postcard is sent. You can start a new verification attempt."
943 elif data.reason == notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_TOO_MANY_ATTEMPTS:
944 reason_message = "Too many incorrect code attempts. You can start a new verification attempt."
945 else:
946 reason_message = (
947 "Your postal verification attempt has failed. You can start a new verification attempt."
948 )
949 return RenderedNotification(
950 is_critical=True,
951 email_subject=title,
952 email_preview=title,
953 email_template_name="security",
954 email_template_args={
955 "title": title,
956 "message": reason_message,
957 },
958 push_title=title,
959 push_body=reason_message,
960 push_icon=urls.icon_url(),
961 push_url=urls.account_settings_link(),
962 )
963 elif notification.topic_action.display == "activeness:probe":
964 title = "Are you still open to hosting on Couchers.org?"
965 return RenderedNotification(
966 email_subject=title,
967 email_preview=title,
968 email_template_name="activeness_probe",
969 email_template_args={
970 "app_link": urls.app_link(),
971 "days_left": (to_aware_datetime(data.deadline) - now()).days,
972 },
973 push_title=title,
974 push_body="Please log in to confirm your hosting status.",
975 push_icon=urls.icon_url(),
976 push_url=urls.app_link(),
977 )
978 elif notification.topic_action.display == "general:new_blog_post":
979 title = f"New blog post: {data.title}"
980 return RenderedNotification(
981 email_subject=title,
982 email_preview=data.blurb,
983 email_template_name="new_blog_post",
984 email_template_args={
985 "title": data.title,
986 "blurb": data.blurb,
987 "url": data.url,
988 },
989 email_topic_action_unsubscribe_text="new blog post alerts",
990 push_title=title,
991 push_body=data.blurb,
992 push_icon=urls.icon_url(),
993 push_url=data.url,
994 )
996 raise NotImplementedError(f"Unknown topic-action: {notification.topic}:{notification.action}")