Coverage for app / backend / src / couchers / notifications / render_email.py: 83%
238 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
1import logging
2from dataclasses import dataclass
3from typing import Any
5from couchers import urls
6from couchers.i18n import LocalizationContext
7from couchers.i18n.localize import format_phone_number
8from couchers.models import Notification, NotificationTopicAction
9from couchers.notifications.quick_links import generate_quick_decline_link, generate_unsub_topic_action
10from couchers.proto import api_pb2, notification_data_pb2
11from couchers.utils import now, to_aware_datetime
13logger = logging.getLogger(__name__)
16@dataclass(kw_only=True)
17class RenderedEmailNotification:
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 subject: str
24 # shows up when listing emails in many clients
25 preview: str
26 # corresponds to .mjml + .txt file in templates/v2
27 template_name: str
28 # other template args
29 template_args: dict[str, Any]
30 # the link label on the topic_action unsubscribe link
31 topic_action_unsubscribe_text: str | None = None
32 # the link label on the topic_key unsubscribe link
33 topic_key_unsubscribe_text: str | None = None
34 # url to unsubscribe with one click
35 list_unsubscribe_url: str | None = None
38def render_email_notification(
39 notification: Notification, loc_context: LocalizationContext
40) -> RenderedEmailNotification:
41 data = notification.topic_action.data_type.FromString(notification.data) # type: ignore[attr-defined]
42 if notification.topic == "host_request":
43 view_link = urls.host_request(host_request_id=data.host_request.host_request_id)
44 if notification.action == "missed_messages":
45 their_your = "their" if data.am_host else "your"
46 other = data.user
47 # "declined your host request", or similar
48 message = f"{other.name} sent you message(s) in {their_your} host request"
49 return RenderedEmailNotification(
50 subject=message,
51 preview=message,
52 template_name="host_request__plain",
53 template_args={
54 "view_link": view_link,
55 "host_request": data.host_request,
56 "message": message,
57 "other": UserTemplateArgs.from_protobuf_user(other),
58 },
59 topic_action_unsubscribe_text="missed messages in host requests",
60 )
61 elif notification.action == "create":
62 other = data.surfer
63 message = f"{other.name} sent you a host request"
64 return RenderedEmailNotification(
65 subject=message,
66 preview=message,
67 template_name="host_request__new",
68 template_args={
69 "view_link": view_link,
70 "quick_decline_link": generate_quick_decline_link(data.host_request),
71 "host_request": data.host_request,
72 "message": message,
73 "other": UserTemplateArgs.from_protobuf_user(other),
74 "text": data.text,
75 },
76 topic_action_unsubscribe_text="new host requests",
77 )
78 elif notification.action == "message": 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true
79 other = data.user
80 if data.am_host:
81 message = f"{other.name} sent you a message in their host request"
82 else:
83 message = f"{other.name} sent you a message in your host request"
84 topic_action_unsub_text = "messages in host request"
85 return RenderedEmailNotification(
86 subject=message,
87 preview=message,
88 template_name="host_request__message",
89 template_args={
90 "view_link": view_link,
91 "host_request": data.host_request,
92 "message": message,
93 "other": UserTemplateArgs.from_protobuf_user(other),
94 "text": data.text,
95 },
96 topic_action_unsubscribe_text=topic_action_unsub_text,
97 )
98 elif notification.action in ["accept", "reject", "confirm", "cancel"]:
99 if notification.action in ["accept", "reject"]:
100 other = data.host
101 their_your = "your"
102 else:
103 other = data.surfer
104 their_your = "their"
105 actioned = {
106 "accept": "accepted",
107 "reject": "declined",
108 "confirm": "confirmed",
109 "cancel": "cancelled",
110 }[notification.action]
111 # "declined your host request", or similar
112 message = f"{other.name} {actioned} {their_your} host request"
113 return RenderedEmailNotification(
114 subject=message,
115 preview=message,
116 template_name="host_request__plain",
117 template_args={
118 "view_link": view_link,
119 "host_request": data.host_request,
120 "message": message,
121 "other": UserTemplateArgs.from_protobuf_user(other),
122 },
123 topic_action_unsubscribe_text=f"{actioned} host requests",
124 )
125 elif notification.action == "reminder": 125 ↛ 767line 125 didn't jump to line 767 because the condition on line 125 was always true
126 message = f"You have a pending host request from {data.surfer.name}!"
127 description = "Please respond to the request!"
128 return RenderedEmailNotification(
129 subject=message,
130 preview=description,
131 template_name="host_request__plain",
132 template_args={
133 "view_link": view_link,
134 "host_request": data.host_request,
135 "message": description,
136 "other": UserTemplateArgs.from_protobuf_user(data.surfer),
137 },
138 topic_action_unsubscribe_text="Pending host request reminders",
139 )
140 elif notification.topic_action == NotificationTopicAction.password__change:
141 title = "Your password was changed"
142 message = "Your login password for Couchers.org was changed."
143 return RenderedEmailNotification(
144 is_critical=True,
145 subject=title,
146 preview=message,
147 template_name="security",
148 template_args={
149 "title": title,
150 "message": message,
151 },
152 )
153 elif notification.topic_action == NotificationTopicAction.password_reset__start:
154 message = "Someone initiated a password change on your account."
155 return RenderedEmailNotification(
156 is_critical=True,
157 subject="Reset your Couchers.org password",
158 preview=message,
159 template_name="password_reset",
160 template_args={
161 "password_reset_link": urls.password_reset_link(password_reset_token=data.password_reset_token)
162 },
163 )
164 elif notification.topic_action == NotificationTopicAction.password_reset__complete:
165 title = "Your password was successfully reset"
166 message = "Your password on Couchers.org was changed. If that was you, then no further action is needed."
167 return RenderedEmailNotification(
168 is_critical=True,
169 subject=title,
170 preview=title,
171 template_name="security",
172 template_args={
173 "title": title,
174 "message": message,
175 },
176 )
177 elif notification.topic_action == NotificationTopicAction.email_address__change:
178 title = "An email change was initiated on your account"
179 message = f"An email change to the email <b>{data.new_email}</b> was initiated on your account."
180 return RenderedEmailNotification(
181 is_critical=True,
182 subject=title,
183 preview=title,
184 template_name="security",
185 template_args={
186 "title": title,
187 "message": message,
188 },
189 )
190 elif notification.topic_action == NotificationTopicAction.email_address__verify:
191 title = "Email change completed"
192 message = "Your new email address has been verified."
193 return RenderedEmailNotification(
194 is_critical=True,
195 subject=title,
196 preview=message,
197 template_name="security",
198 template_args={
199 "title": title,
200 "message": message,
201 },
202 )
203 elif notification.topic_action == NotificationTopicAction.phone_number__change:
204 title = "Phone verification started"
205 message = f"You started phone number verification with the number <b>{format_phone_number(data.phone)}</b>."
206 return RenderedEmailNotification(
207 is_critical=True,
208 subject=title,
209 preview=message,
210 template_name="security",
211 template_args={
212 "title": title,
213 "message": message,
214 },
215 )
216 elif notification.topic_action == NotificationTopicAction.phone_number__verify:
217 title = "Phone successfully verified"
218 message = f"Your phone was successfully verified as <b>{format_phone_number(data.phone)}</b> on Couchers.org."
219 message_plain = f"Your phone was successfully verified as {format_phone_number(data.phone)} on Couchers.org."
220 return RenderedEmailNotification(
221 is_critical=True,
222 subject=title,
223 preview=message_plain,
224 template_name="security",
225 template_args={
226 "title": title,
227 "message": message,
228 },
229 )
230 elif notification.topic_action == NotificationTopicAction.gender__change:
231 title = "Your gender was changed"
232 message = f"Your gender on Couchers.org was changed to <b>{data.gender}</b> by an admin."
233 message_plain = f"Your gender on Couchers.org was changed to {data.gender} by an admin."
234 return RenderedEmailNotification(
235 is_critical=True,
236 subject=title,
237 preview=message_plain,
238 template_name="security",
239 template_args={
240 "title": title,
241 "message": message,
242 },
243 )
244 elif notification.topic_action == NotificationTopicAction.birthdate__change:
245 title = "Your date of birth was changed"
246 birthdate = loc_context.localize_date_from_iso(data.birthdate)
247 message = f"Your date of birth on Couchers.org was changed to <b>{birthdate}</b> by an admin."
248 message_plain = f"Your date of birth on Couchers.org was changed to {birthdate} by an admin."
249 return RenderedEmailNotification(
250 is_critical=True,
251 subject=title,
252 preview=message_plain,
253 template_name="security",
254 template_args={
255 "title": title,
256 "message": message,
257 },
258 )
259 elif notification.topic_action == NotificationTopicAction.api_key__create:
260 return RenderedEmailNotification(
261 is_critical=True,
262 subject="Your API key for Couchers.org",
263 preview="We have issued you an API key as per your request.",
264 template_name="api_key",
265 template_args={
266 "api_key": data.api_key,
267 "expiry": data.expiry,
268 },
269 )
270 elif notification.topic_action.display in ["badge:add", "badge:remove"]:
271 actioned = "added to" if notification.action == "add" else "removed from"
272 title = f"The {data.badge_name} badge was {actioned} your profile"
273 return RenderedEmailNotification(
274 subject=title,
275 preview=title,
276 template_name="badge",
277 template_args={
278 "badge_name": data.badge_name,
279 "actioned": actioned,
280 "unsub_type": "badge additions" if notification.action == "add" else "badge removals",
281 },
282 topic_action_unsubscribe_text="badge additions" if notification.action == "add" else "badge removals",
283 list_unsubscribe_url=generate_unsub_topic_action(notification),
284 )
285 elif notification.topic_action == NotificationTopicAction.donation__received:
286 title = loc_context.localize_string("notifications.donation_received.title")
287 message = loc_context.localize_string(
288 "notifications.donation_received.thanks_amount",
289 substitutions={
290 "amount": data.amount,
291 },
292 )
293 return RenderedEmailNotification(
294 is_critical=True,
295 subject=title,
296 preview=message,
297 template_name="donation_received",
298 template_args={
299 "amount": data.amount,
300 "receipt_url": data.receipt_url,
301 },
302 )
303 elif notification.topic_action == NotificationTopicAction.friend_request__create:
304 other = data.other_user
305 preview = f"You've received a friend request from {other.name}"
306 return RenderedEmailNotification(
307 subject=f"{other.name} wants to be your friend on Couchers.org!",
308 preview=preview,
309 template_name="friend_request",
310 template_args={
311 "friend_requests_link": urls.friend_requests_link(),
312 "other": UserTemplateArgs.from_protobuf_user(other),
313 },
314 topic_action_unsubscribe_text="new friend requests",
315 )
316 elif notification.topic_action == NotificationTopicAction.friend_request__accept:
317 other = data.other_user
318 title = f"{other.name} accepted your friend request!"
319 preview = f"{other.name} has accepted your friend request"
320 return RenderedEmailNotification(
321 subject=title,
322 preview=preview,
323 template_name="friend_request_accepted",
324 template_args={
325 "other": UserTemplateArgs.from_protobuf_user(other),
326 },
327 topic_action_unsubscribe_text="accepted friend requests",
328 )
329 elif notification.topic_action == NotificationTopicAction.account_deletion__start:
330 return RenderedEmailNotification(
331 is_critical=True,
332 allow_deleted=True,
333 subject="Confirm your Couchers.org account deletion",
334 preview="Please confirm that you want to delete your Couchers.org account.",
335 template_name="account_deletion_start",
336 template_args={
337 "deletion_link": urls.delete_account_link(account_deletion_token=data.deletion_token),
338 },
339 )
340 elif notification.topic_action == NotificationTopicAction.account_deletion__complete:
341 title = "Your Couchers.org account has been deleted"
342 return RenderedEmailNotification(
343 is_critical=True,
344 allow_deleted=True,
345 subject=title,
346 preview="We have deleted your Couchers.org account, to undo, follow the link in this email.",
347 template_name="account_deletion_complete",
348 template_args={
349 "undelete_link": urls.recover_account_link(account_undelete_token=data.undelete_token),
350 "days": data.undelete_days,
351 },
352 )
353 elif notification.topic_action == NotificationTopicAction.account_deletion__recovered:
354 title = "Your Couchers.org account has been recovered!"
355 subtitle = "We have recovered your Couchers.org account as per your request! Welcome back!"
356 return RenderedEmailNotification(
357 is_critical=True,
358 allow_deleted=True,
359 subject=title,
360 preview=subtitle,
361 template_name="account_deletion_recovered",
362 template_args={
363 "app_link": urls.app_link(),
364 },
365 )
366 elif notification.topic_action == NotificationTopicAction.chat__message:
367 return RenderedEmailNotification(
368 subject=data.message,
369 preview="You received a message on Couchers.org!",
370 template_name="chat_message",
371 template_args={
372 "author": UserTemplateArgs.from_protobuf_user(data.author),
373 "message": data.message,
374 "text": data.text,
375 "view_link": urls.chat_link(chat_id=data.group_chat_id),
376 },
377 topic_action_unsubscribe_text="new chat messages",
378 topic_key_unsubscribe_text="this chat (mute)",
379 )
380 elif notification.topic_action == NotificationTopicAction.chat__missed_messages:
381 return RenderedEmailNotification(
382 subject="You have unseen messages on Couchers.org!",
383 preview="You missed some messages on the platform.",
384 template_name="chat_unseen_messages",
385 template_args={
386 "items": [
387 {
388 "author": UserTemplateArgs.from_protobuf_user(item.author),
389 "message": item.message,
390 "text": item.text,
391 "view_link": urls.chat_link(chat_id=item.group_chat_id),
392 }
393 for item in data.messages
394 ]
395 },
396 topic_action_unsubscribe_text="unseen chat messages",
397 )
398 elif notification.topic == "event":
399 event = data.event
400 start_time = loc_context.localize_datetime(event.start_time)
401 end_time = loc_context.localize_datetime(event.end_time)
402 time_display = f"{start_time} - {end_time}"
403 event_link = urls.event_link(occurrence_id=event.event_id, slug=event.slug)
404 if notification.action in ["create_approved", "create_any"]:
405 # create_approved = invitation, approved by mods
406 # create_any = new event created by anyone (no need for approval) -- off by default
407 if notification.action == "create_approved": 407 ↛ 410line 407 didn't jump to line 410 because the condition on line 407 was always true
408 subject = f'{data.inviting_user.name} invited you to "{event.title}"'
409 start_text = "You've been invited to a new event"
410 elif notification.action == "create_any":
411 subject = f'{data.inviting_user.name} created an event called "{event.title}"'
412 start_text = "A new event was created"
413 community_link = (
414 urls.community_link(node_id=data.in_community.community_id, slug=data.in_community.slug)
415 if data.in_community
416 else None
417 )
418 return RenderedEmailNotification(
419 subject=subject,
420 preview=f"{start_text} on Couchers.org!",
421 template_name="event_create",
422 template_args={
423 "inviting_user": UserTemplateArgs.from_protobuf_user(data.inviting_user),
424 "time_display": time_display,
425 "start_text": start_text,
426 "nearby": "nearby" if data.nearby else None,
427 "community": data.in_community if data.in_community else None,
428 "community_link": community_link,
429 "event": event,
430 "view_link": event_link,
431 },
432 topic_action_unsubscribe_text=(
433 "new events by community members"
434 if notification.action == "create_any"
435 else "invitations to events (approved by moderators)"
436 ),
437 )
438 elif notification.action == "update": 438 ↛ 439line 438 didn't jump to line 439 because the condition on line 438 was never true
439 updated_text = ", ".join(data.updated_items)
440 return RenderedEmailNotification(
441 subject=f'{data.updating_user.name} updated "{event.title}"',
442 preview="An event you are subscribed to was updated.",
443 template_name="event_update",
444 template_args={
445 "updating_user": UserTemplateArgs.from_protobuf_user(data.updating_user),
446 "time_display": time_display,
447 "event": event,
448 "updated_text": updated_text,
449 "view_link": event_link,
450 },
451 topic_action_unsubscribe_text="event updates",
452 )
453 elif notification.action == "cancel": 453 ↛ 454line 453 didn't jump to line 454 because the condition on line 453 was never true
454 return RenderedEmailNotification(
455 subject=f'{data.cancelling_user.name} cancelled "{event.title}"',
456 preview="An event you are subscribed to has been cancelled.",
457 template_name="event_cancel",
458 template_args={
459 "cancelling_user": UserTemplateArgs.from_protobuf_user(data.cancelling_user),
460 "time_display": time_display,
461 "event": event,
462 "view_link": event_link,
463 },
464 topic_action_unsubscribe_text="event cancellations",
465 )
466 elif notification.action == "delete": 466 ↛ 467line 466 didn't jump to line 467 because the condition on line 466 was never true
467 return RenderedEmailNotification(
468 subject=f'A moderator deleted "{event.title}"',
469 preview="An event you are subscribed to has been deleted.",
470 template_name="event_delete",
471 template_args={
472 "time_display": time_display,
473 "event": event,
474 },
475 topic_action_unsubscribe_text="event deletions",
476 )
477 elif notification.action == "invite_organizer": 477 ↛ 478line 477 didn't jump to line 478 because the condition on line 477 was never true
478 return RenderedEmailNotification(
479 subject=f'{data.inviting_user.name} invited you to co-organize "{event.title}"',
480 preview="You were invited to co-organize an event on Couchers.org.",
481 template_name="event_invite_organizer",
482 template_args={
483 "inviting_user": UserTemplateArgs.from_protobuf_user(data.inviting_user),
484 "time_display": time_display,
485 "event": event,
486 "view_link": event_link,
487 },
488 topic_action_unsubscribe_text="invitations to co-organize events",
489 )
490 elif notification.action == "comment":
491 return RenderedEmailNotification(
492 subject=f'{data.author.name} commented on "{event.title}"',
493 preview="Someone commented on an event you are attending.",
494 template_name="event_comment",
495 template_args={
496 "author": UserTemplateArgs.from_protobuf_user(data.author),
497 "time_display": time_display,
498 "event": event,
499 "content": data.reply.content,
500 "view_link": event_link,
501 },
502 topic_action_unsubscribe_text="event comments",
503 )
504 elif notification.action == "reminder": 504 ↛ 767line 504 didn't jump to line 767 because the condition on line 504 was always true
505 return RenderedEmailNotification(
506 subject=f'Reminder: "{data.event.title}" starts soon',
507 preview="Don't forget your upcoming event on Couchers.org",
508 template_name="event_reminder",
509 template_args={
510 "time_display": time_display,
511 "event": event,
512 "view_link": event_link,
513 },
514 topic_action_unsubscribe_text="event reminders",
515 )
516 elif notification.topic == "discussion":
517 discussion = data.discussion
518 discussion_link = urls.discussion_link(discussion_id=discussion.discussion_id, slug=discussion.slug)
519 if notification.action == "create": 519 ↛ 520line 519 didn't jump to line 520 because the condition on line 519 was never true
520 return RenderedEmailNotification(
521 subject=f'{data.author.name} created a discussion: "{discussion.title}"',
522 preview="Someone created a discussion in a community or group you are subscribed to.",
523 template_name="discussion_create",
524 template_args={
525 "author": UserTemplateArgs.from_protobuf_user(data.author),
526 "discussion": discussion,
527 "view_link": discussion_link,
528 },
529 topic_action_unsubscribe_text="new discussions",
530 )
531 elif notification.action == "comment": 531 ↛ 767line 531 didn't jump to line 767 because the condition on line 531 was always true
532 return RenderedEmailNotification(
533 subject=f'{data.author.name} commented on "{discussion.title}"',
534 preview="Someone commented on your discussion.",
535 template_name="discussion_comment",
536 template_args={
537 "author": UserTemplateArgs.from_protobuf_user(data.author),
538 "discussion": discussion,
539 "reply": data.reply,
540 "view_link": discussion_link,
541 },
542 topic_action_unsubscribe_text="discussion comments",
543 )
544 elif notification.topic_action == NotificationTopicAction.thread__reply:
545 parent = data.WhichOneof("reply_parent")
546 if parent == "event":
547 title = data.event.title
548 view_link = urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug)
549 elif parent == "discussion": 549 ↛ 553line 549 didn't jump to line 553 because the condition on line 549 was always true
550 title = data.discussion.title
551 view_link = urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug)
552 else:
553 raise Exception("Can only do replies to events and discussions")
555 return RenderedEmailNotification(
556 subject=f'{data.author.name} replied in "{title}"',
557 preview="Someone replied in a comment thread you have participated in.",
558 template_name="comment_reply",
559 template_args={
560 "author": UserTemplateArgs.from_protobuf_user(data.author),
561 "title": title,
562 "reply": data.reply,
563 "view_link": view_link,
564 },
565 topic_action_unsubscribe_text="comment replies",
566 )
567 elif notification.topic == "reference":
568 if notification.action == "receive_friend":
569 title = f"You've received a friend reference from {data.from_user.name}!"
570 return RenderedEmailNotification(
571 subject=title,
572 preview=data.text,
573 template_name="friend_reference",
574 template_args={
575 "from_user": UserTemplateArgs.from_protobuf_user(data.from_user),
576 "profile_references_link": urls.profile_references_link(),
577 "text": data.text,
578 },
579 topic_action_unsubscribe_text="new references from friends",
580 )
581 elif notification.action in ["receive_hosted", "receive_surfed"]:
582 title = f"You've received a reference from {data.from_user.name}!"
583 # what was my type? i surfed with them if i received a "hosted" request
584 surfed = notification.action == "receive_hosted"
585 leave_reference_link = urls.leave_reference_link(
586 reference_type="surfed" if surfed else "hosted",
587 to_user_id=data.from_user.user_id,
588 host_request_id=data.host_request_id,
589 )
590 profile_references_link = urls.profile_references_link()
591 if data.text: 591 ↛ 592line 591 didn't jump to line 592 because the condition on line 591 was never true
592 preview = data.text
593 else:
594 preview = "Please go and write a reference for them too. It's a nice gesture and helps us build a community together!"
595 return RenderedEmailNotification(
596 subject=title,
597 preview=preview,
598 template_name="host_reference",
599 template_args={
600 "from_user": UserTemplateArgs.from_protobuf_user(data.from_user),
601 "leave_reference_link": leave_reference_link,
602 "profile_references_link": profile_references_link,
603 "text": data.text,
604 "both_written": True if data.text else False,
605 "surfed": surfed,
606 },
607 topic_action_unsubscribe_text="new references from " + ("hosts" if surfed else "surfers"),
608 )
609 elif notification.action in ["reminder_hosted", "reminder_surfed"]: 609 ↛ 767line 609 didn't jump to line 767 because the condition on line 609 was always true
610 # what was my type? i surfed with them if i get a surfed reminder
611 surfed = notification.action == "reminder_surfed"
612 leave_reference_link = urls.leave_reference_link(
613 reference_type="surfed" if surfed else "hosted",
614 to_user_id=data.other_user.user_id,
615 host_request_id=data.host_request_id,
616 )
617 title = f"You have {data.days_left} days to write a reference for {data.other_user.name}!"
618 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."
619 return RenderedEmailNotification(
620 subject=title,
621 preview=preview,
622 template_name="reference_reminder",
623 template_args={
624 "other_user": UserTemplateArgs.from_protobuf_user(data.other_user),
625 "leave_reference_link": leave_reference_link,
626 "days_left": str(data.days_left),
627 "surfed": surfed,
628 },
629 topic_action_unsubscribe_text=("surfed" if surfed else "hosted") + " reference reminders",
630 )
631 elif notification.topic_action == NotificationTopicAction.onboarding__reminder:
632 if notification.key == "1":
633 return RenderedEmailNotification(
634 subject="Welcome to Couchers.org and the future of couch surfing",
635 preview="We are so excited to have you join our community!",
636 template_name="onboarding1",
637 template_args={
638 "app_link": urls.app_link(),
639 "edit_profile_link": urls.edit_profile_link(),
640 },
641 topic_action_unsubscribe_text="onboarding emails",
642 )
643 elif notification.key == "2": 643 ↛ 767line 643 didn't jump to line 767 because the condition on line 643 was always true
644 return RenderedEmailNotification(
645 subject="Complete your profile on Couchers.org",
646 preview="We would ask one big favour of you: please fill out your profile by adding a photo and some text.",
647 template_name="onboarding2",
648 template_args={
649 "edit_profile_link": urls.edit_profile_link(),
650 },
651 topic_action_unsubscribe_text="onboarding emails",
652 )
653 elif notification.topic_action == NotificationTopicAction.modnote__create:
654 title = "You have received a mod note"
655 message = "You have received an important note from the moderators. You must read and acknowledge it before continuing to use the platform."
656 return RenderedEmailNotification(
657 is_critical=True,
658 subject=title,
659 preview=message,
660 template_name="mod_note",
661 template_args={"title": title},
662 )
663 elif notification.topic_action == NotificationTopicAction.verification__sv_success:
664 title = "Strong Verification succeeded"
665 message = "You have been verified with Strong Verification! You will now see a tick next to your name on the platform."
666 return RenderedEmailNotification(
667 is_critical=True,
668 subject=title,
669 preview=message,
670 template_name="strong_verification_success",
671 template_args={
672 "message": message,
673 },
674 )
675 elif notification.topic_action == NotificationTopicAction.verification__sv_fail:
676 title = "Strong Verification failed"
677 if data.reason == notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER: 677 ↛ 678line 677 didn't jump to line 678 because the condition on line 677 was never true
678 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."
679 elif data.reason == notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT:
680 reason_message = "You tried to verify with a document that is not a passport. You can only use a passport for Strong Verification."
681 elif data.reason == notification_data_pb2.SV_FAIL_REASON_DUPLICATE: 681 ↛ 684line 681 didn't jump to line 684 because the condition on line 681 was always true
682 reason_message = "You tried to verify with a passport that has already been used for verification. Please use another passport."
683 else:
684 raise Exception("Shouldn't get here")
685 return RenderedEmailNotification(
686 is_critical=True,
687 subject=title,
688 preview=title,
689 template_name="security",
690 template_args={
691 "title": title,
692 "message": reason_message,
693 },
694 )
695 elif notification.topic == "postal_verification":
696 if notification.action == "postcard_sent": 696 ↛ 709line 696 didn't jump to line 709 because the condition on line 696 was always true
697 title = "Your verification postcard is on its way"
698 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."
699 return RenderedEmailNotification(
700 is_critical=True,
701 subject=title,
702 preview=message,
703 template_name="security",
704 template_args={
705 "title": title,
706 "message": message,
707 },
708 )
709 elif notification.action == "success":
710 title = "Postal Verification succeeded"
711 message = "You have been verified with Postal Verification! Your address has been confirmed."
712 return RenderedEmailNotification(
713 is_critical=True,
714 subject=title,
715 preview=message,
716 template_name="security",
717 template_args={
718 "title": title,
719 "message": message,
720 },
721 )
722 elif notification.action == "failed":
723 title = "Postal Verification failed"
724 if data.reason == notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_CODE_EXPIRED:
725 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."
726 elif data.reason == notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_TOO_MANY_ATTEMPTS:
727 reason_message = "Too many incorrect code attempts. You can start a new verification attempt."
728 else:
729 reason_message = (
730 "Your postal verification attempt has failed. You can start a new verification attempt."
731 )
732 return RenderedEmailNotification(
733 is_critical=True,
734 subject=title,
735 preview=title,
736 template_name="security",
737 template_args={
738 "title": title,
739 "message": reason_message,
740 },
741 )
742 elif notification.topic_action == NotificationTopicAction.activeness__probe:
743 title = "Are you still open to hosting on Couchers.org?"
744 return RenderedEmailNotification(
745 subject=title,
746 preview=title,
747 template_name="activeness_probe",
748 template_args={
749 "app_link": urls.app_link(),
750 "days_left": (to_aware_datetime(data.deadline) - now()).days,
751 },
752 )
753 elif notification.topic_action == NotificationTopicAction.general__new_blog_post: 753 ↛ 767line 753 didn't jump to line 767 because the condition on line 753 was always true
754 title = f"New blog post: {data.title}"
755 return RenderedEmailNotification(
756 subject=title,
757 preview=data.blurb,
758 template_name="new_blog_post",
759 template_args={
760 "title": data.title,
761 "blurb": data.blurb,
762 "url": data.url,
763 },
764 topic_action_unsubscribe_text="new blog post alerts",
765 )
767 raise NotImplementedError(f"Unknown topic-action: {notification.topic}:{notification.action}")
770@dataclass(frozen=True, slots=True, kw_only=True)
771class UserTemplateArgs:
772 """
773 A user's information for email template placeholders.
774 Allows decoupling from protocol buffer objects.
775 """
777 name: str
778 age: int
779 city: str
780 avatar_url: str
781 profile_url: str
783 @staticmethod
784 def from_protobuf_user(user: api_pb2.User) -> UserTemplateArgs:
785 return UserTemplateArgs(
786 name=user.name,
787 age=user.age,
788 city=user.city,
789 avatar_url=user.avatar_thumbnail_url or urls.icon_url(),
790 profile_url=urls.user_link(username=user.username),
791 )