Coverage for src/couchers/notifications/render.py: 87%

243 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-07-03 04:21 +0000

1import logging 

2from dataclasses import dataclass 

3 

4from couchers import urls 

5from couchers.notifications.unsubscribe import generate_unsub_topic_action 

6from couchers.templates.v2 import v2avatar, v2date, v2esc, v2phone, v2timestamp 

7from couchers.utils import now, to_aware_datetime 

8from proto import notification_data_pb2 

9 

10logger = logging.getLogger(__name__) 

11 

12 

13@dataclass(kw_only=True) 

14class RenderedNotification: 

15 # whether the notification is critical and cannot be turned off 

16 is_critical: bool = False 

17 # whether this email can be sent to someone who is deleted 

18 allow_deleted: bool = False 

19 # email subject 

20 email_subject: str 

21 # shows up when listing emails in many clients 

22 email_preview: str 

23 # corresponds to .mjml + .txt file in templates/v2 

24 email_template_name: str 

25 # other template args 

26 email_template_args: dict 

27 # the link label on the topic_action unsubscribe link 

28 email_topic_action_unsubscribe_text: str = None 

29 # the link label on the topic_key unsubscribe link 

30 email_topic_key_unsubscribe_text: str = None 

31 # url to unsubscribe with one click 

32 email_list_unsubscribe_url: str = None 

33 # push notification title 

34 push_title: str 

35 # push notification content 

36 push_body: str 

37 # url to an icon for push notifications 

38 push_icon: str 

39 # url to where clicking on the notification should take you 

40 push_url: str 

41 

42 

43def render_notification(user, notification) -> RenderedNotification: 

44 data = notification.topic_action.data_type.FromString(notification.data) 

45 if notification.topic == "host_request": 

46 view_link = urls.host_request(host_request_id=data.host_request.host_request_id) 

47 if notification.action == "missed_messages": 

48 their_your = "their" if data.am_host else "your" 

49 other = data.user 

50 # "declined your host request", or similar 

51 message = f"{other.name} sent you message(s) in {their_your} host request" 

52 return RenderedNotification( 

53 email_subject=message, 

54 email_preview=message, 

55 email_template_name="host_request__plain", 

56 email_template_args={ 

57 "view_link": view_link, 

58 "host_request": data.host_request, 

59 "message": message, 

60 "other": other, 

61 }, 

62 email_topic_action_unsubscribe_text="missed messages in host requests", 

63 push_title=message, 

64 push_body="Check the app for more info.", 

65 push_icon=v2avatar(other), 

66 push_url=view_link, 

67 ) 

68 elif notification.action in ["create", "message"]: 

69 if notification.action == "create": 

70 other = data.surfer 

71 message = f"{other.name} sent you a host request" 

72 topic_action_unsub_text = "new host requests" 

73 elif notification.action == "message": 

74 other = data.user 

75 if data.am_host: 

76 message = f"{other.name} sent you a message in their host request" 

77 else: 

78 message = f"{other.name} sent you a message in your host request" 

79 topic_action_unsub_text = "messages in host request" 

80 return RenderedNotification( 

81 email_subject=message, 

82 email_preview=message, 

83 email_template_name="host_request__message", 

84 email_template_args={ 

85 "view_link": view_link, 

86 "host_request": data.host_request, 

87 "message": message, 

88 "other": other, 

89 "text": data.text, 

90 }, 

91 email_topic_action_unsubscribe_text=topic_action_unsub_text, 

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 in ["accept", "reject", "confirm", "cancel"]: 

98 if notification.action in ["accept", "reject"]: 

99 other = data.host 

100 their_your = "your" 

101 else: 

102 other = data.surfer 

103 their_your = "their" 

104 actioned = { 

105 "accept": "accepted", 

106 "reject": "declined", 

107 "confirm": "confirmed", 

108 "cancel": "cancelled", 

109 }[notification.action] 

110 # "declined your host request", or similar 

111 message = f"{other.name} {actioned} {their_your} host request" 

112 return RenderedNotification( 

113 email_subject=message, 

114 email_preview=message, 

115 email_template_name="host_request__plain", 

116 email_template_args={ 

117 "view_link": view_link, 

118 "host_request": data.host_request, 

119 "message": message, 

120 "other": other, 

121 }, 

122 email_topic_action_unsubscribe_text=f"{actioned} host requests", 

123 push_title=message, 

124 push_body="Check the app for more info.", 

125 push_icon=v2avatar(other), 

126 push_url=view_link, 

127 ) 

128 elif notification.action == "reminder": 

129 message = f"You have a pending host request from {data.surfer.name}!" 

130 description = "Please respond to the request!" 

131 return RenderedNotification( 

132 email_subject=message, 

133 email_preview=description, 

134 email_template_name="host_request__plain", 

135 email_template_args={ 

136 "view_link": view_link, 

137 "host_request": data.host_request, 

138 "message": description, 

139 "other": data.surfer, 

140 }, 

141 email_topic_action_unsubscribe_text="Pending host request reminders", 

142 push_title=message, 

143 push_body=description, 

144 push_icon=v2avatar(data.surfer), 

145 push_url=view_link, 

146 ) 

147 elif notification.topic_action.display == "password:change": 

148 title = "Your password was changed" 

149 message = "Your login password for Couchers.org was changed." 

150 return RenderedNotification( 

151 is_critical=True, 

152 email_subject=title, 

153 email_preview=message, 

154 email_template_name="security", 

155 email_template_args={ 

156 "title": title, 

157 "message": message, 

158 }, 

159 push_title=title, 

160 push_body=message, 

161 push_icon=urls.icon_url(), 

162 push_url=urls.account_settings_link(), 

163 ) 

164 elif notification.topic_action.display == "password_reset:start": 

165 message = "Someone initiated a password change on your account." 

166 return RenderedNotification( 

167 is_critical=True, 

168 email_subject="Reset your Couchers.org password", 

169 email_preview=message, 

170 email_template_name="password_reset", 

171 email_template_args={ 

172 "password_reset_link": urls.password_reset_link(password_reset_token=data.password_reset_token) 

173 }, 

174 push_title="A password reset was initiated on your account", 

175 push_body=message, 

176 push_icon=urls.icon_url(), 

177 push_url=urls.account_settings_link(), 

178 ) 

179 elif notification.topic_action.display == "password_reset:complete": 

180 title = "Your password was successfully reset" 

181 message = "Your password on Couchers.org was changed. If that was you, then no further action is needed." 

182 return RenderedNotification( 

183 is_critical=True, 

184 email_subject=title, 

185 email_preview=title, 

186 email_template_name="security", 

187 email_template_args={ 

188 "title": title, 

189 "message": message, 

190 }, 

191 push_title=title, 

192 push_body=message, 

193 push_icon=urls.icon_url(), 

194 push_url=urls.account_settings_link(), 

195 ) 

196 elif notification.topic_action.display == "email_address:change": 

197 title = "An email change was initiated on your account" 

198 message = f"An email change to the email <b>{data.new_email}</b> was initiated on your account." 

199 message_plain = f"An email change to the email {data.new_email} was initiated on your account." 

200 return RenderedNotification( 

201 is_critical=True, 

202 email_subject=title, 

203 email_preview=title, 

204 email_template_name="security", 

205 email_template_args={ 

206 "title": title, 

207 "message": message, 

208 }, 

209 push_title=title, 

210 push_body=message_plain, 

211 push_icon=urls.icon_url(), 

212 push_url=urls.account_settings_link(), 

213 ) 

214 elif notification.topic_action.display == "email_address:verify": 

215 title = "Email change completed" 

216 message = "Your new email address has been verified." 

217 return RenderedNotification( 

218 is_critical=True, 

219 email_subject=title, 

220 email_preview=message, 

221 email_template_name="security", 

222 email_template_args={ 

223 "title": title, 

224 "message": message, 

225 }, 

226 push_title=title, 

227 push_body=message, 

228 push_icon=urls.icon_url(), 

229 push_url=urls.account_settings_link(), 

230 ) 

231 elif notification.topic_action.display == "phone_number:change": 

232 title = "Phone verification started" 

233 message = f"You started phone number verification with the number <b>{v2phone(data.phone)}</b>." 

234 message_plain = f"You started phone number verification with the number {v2phone(data.phone)}." 

235 return RenderedNotification( 

236 is_critical=True, 

237 email_subject=title, 

238 email_preview=message, 

239 email_template_name="security", 

240 email_template_args={ 

241 "title": title, 

242 "message": message, 

243 }, 

244 push_title=title, 

245 push_body=message_plain, 

246 push_icon=urls.icon_url(), 

247 push_url=urls.feature_preview_link(), 

248 ) 

249 elif notification.topic_action.display == "phone_number:verify": 

250 title = "Phone successfully verified" 

251 message = f"Your phone was successfully verified as <b>{v2phone(data.phone)}</b> on Couchers.org." 

252 message_plain = f"Your phone was successfully verified as {v2phone(data.phone)} on Couchers.org." 

253 return RenderedNotification( 

254 is_critical=True, 

255 email_subject=title, 

256 email_preview=message_plain, 

257 email_template_name="security", 

258 email_template_args={ 

259 "title": title, 

260 "message": message, 

261 }, 

262 push_title=title, 

263 push_body=message_plain, 

264 push_icon=urls.icon_url(), 

265 push_url=urls.feature_preview_link(), 

266 ) 

267 elif notification.topic_action.display == "gender:change": 

268 title = "Your gender was changed" 

269 message = f"Your gender on Couchers.org was changed to <b>{data.gender}</b> by an admin." 

270 message_plain = f"Your gender on Couchers.org was changed to {data.gender} by an admin." 

271 return RenderedNotification( 

272 is_critical=True, 

273 email_subject=title, 

274 email_preview=message_plain, 

275 email_template_name="security", 

276 email_template_args={ 

277 "title": title, 

278 "message": message, 

279 }, 

280 push_title=title, 

281 push_body=message_plain, 

282 push_icon=urls.icon_url(), 

283 push_url=urls.account_settings_link(), 

284 ) 

285 elif notification.topic_action.display == "birthdate:change": 

286 title = "Your date of birth was changed" 

287 message = ( 

288 f"Your date of birth on Couchers.org was changed to <b>{v2date(data.birthdate, user)}</b> by an admin." 

289 ) 

290 message_plain = f"Your date of birth on Couchers.org was changed to {v2date(data.birthdate, user)} by an admin." 

291 return RenderedNotification( 

292 is_critical=True, 

293 email_subject=title, 

294 email_preview=message_plain, 

295 email_template_name="security", 

296 email_template_args={ 

297 "title": title, 

298 "message": message, 

299 }, 

300 push_title=title, 

301 push_body=message_plain, 

302 push_icon=urls.icon_url(), 

303 push_url=urls.account_settings_link(), 

304 ) 

305 elif notification.topic_action.display == "api_key:create": 

306 return RenderedNotification( 

307 is_critical=True, 

308 email_subject="Your API key for Couchers.org", 

309 email_preview="We have issued you an API key as per your request.", 

310 email_template_name="api_key", 

311 email_template_args={ 

312 "api_key": data.api_key, 

313 "expiry": data.expiry, 

314 }, 

315 push_title="An API key was created for your account", 

316 push_body="Details were sent to you via email.", 

317 push_icon=urls.icon_url(), 

318 push_url=urls.app_link(), 

319 ) 

320 elif notification.topic_action.display in ["badge:add", "badge:remove"]: 

321 actioned = "added to" if notification.action == "add" else "removed from" 

322 title = f"The {data.badge_name} badge was {actioned} your profile" 

323 return RenderedNotification( 

324 email_subject=title, 

325 email_preview=title, 

326 email_template_name="badge", 

327 email_template_args={ 

328 "badge_name": data.badge_name, 

329 "actioned": actioned, 

330 "unsub_type": "badge additions" if notification.action == "add" else "badge removals", 

331 }, 

332 email_topic_action_unsubscribe_text="badge additions" if notification.action == "add" else "badge removals", 

333 push_title=title, 

334 push_body=( 

335 "Check out your profile to see the new badge!" 

336 if notification.action == "add" 

337 else "You can see all your badges on your profile." 

338 ), 

339 push_icon=urls.icon_url(), 

340 push_url=urls.profile_link(), 

341 email_list_unsubscribe_url=generate_unsub_topic_action(notification), 

342 ) 

343 elif notification.topic_action.display == "donation:received": 

344 title = "Thank you for your donation to Couchers.org!" 

345 message = f"Thank you so much for your donation of ${data.amount} to Couchers.org." 

346 return RenderedNotification( 

347 is_critical=True, 

348 email_subject=title, 

349 email_preview=message, 

350 email_template_name="donation_received", 

351 email_template_args={ 

352 "amount": data.amount, 

353 "receipt_url": data.receipt_url, 

354 }, 

355 push_title=title, 

356 push_body=message, 

357 push_icon=urls.icon_url(), 

358 push_url=data.receipt_url, 

359 ) 

360 elif notification.topic_action.display == "friend_request:create": 

361 other = data.other_user 

362 preview = f"You've received a friend request from {other.name}" 

363 return RenderedNotification( 

364 email_subject=f"{other.name} wants to be your friend on Couchers.org!", 

365 email_preview=preview, 

366 email_template_name="friend_request", 

367 email_template_args={ 

368 "friend_requests_link": urls.friend_requests_link(), 

369 "other": other, 

370 }, 

371 email_topic_action_unsubscribe_text="new friend requests", 

372 push_title=f"{other.name} wants to be your friend", 

373 push_body=preview, 

374 push_icon=v2avatar(other), 

375 push_url=urls.friend_requests_link(), 

376 ) 

377 elif notification.topic_action.display == "friend_request:accept": 

378 other = data.other_user 

379 title = f"{other.name} accepted your friend request!" 

380 preview = f"{v2esc(other.name)} has accepted your friend request" 

381 return RenderedNotification( 

382 email_subject=title, 

383 email_preview=preview, 

384 email_template_name="friend_request_accepted", 

385 email_template_args={ 

386 "other_user_link": urls.user_link(username=other.username), 

387 "other": other, 

388 }, 

389 email_topic_action_unsubscribe_text="accepted friend requests", 

390 push_title=title, 

391 push_body=preview, 

392 push_icon=v2avatar(other), 

393 push_url=urls.user_link(username=other.username), 

394 ) 

395 elif notification.topic_action.display == "account_deletion:start": 

396 return RenderedNotification( 

397 is_critical=True, 

398 allow_deleted=True, 

399 email_subject="Confirm your Couchers.org account deletion", 

400 email_preview="Please confirm that you want to delete your Couchers.org account.", 

401 email_template_name="account_deletion_start", 

402 email_template_args={ 

403 "deletion_link": urls.delete_account_link(account_deletion_token=data.deletion_token), 

404 }, 

405 push_title="Account deletion initiated", 

406 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.", 

407 push_icon=urls.icon_url(), 

408 push_url=urls.app_link(), 

409 ) 

410 elif notification.topic_action.display == "account_deletion:complete": 

411 title = "Your Couchers.org account has been deleted" 

412 return RenderedNotification( 

413 is_critical=True, 

414 allow_deleted=True, 

415 email_subject=title, 

416 email_preview="We have deleted your Couchers.org account, to undo, follow the link in this email.", 

417 email_template_name="account_deletion_complete", 

418 email_template_args={ 

419 "undelete_link": urls.recover_account_link(account_undelete_token=data.undelete_token), 

420 "days": data.undelete_days, 

421 }, 

422 push_title=title, 

423 push_body=f"You can still undo this by following the link we emailed to you within {data.undelete_days} days.", 

424 push_icon=urls.icon_url(), 

425 push_url=urls.app_link(), 

426 ) 

427 elif notification.topic_action.display == "account_deletion:recovered": 

428 title = "Your Couchers.org account has been recovered!" 

429 subtitle = "We have recovered your Couchers.org account as per your request! Welcome back!" 

430 return RenderedNotification( 

431 is_critical=True, 

432 allow_deleted=True, 

433 email_subject=title, 

434 email_preview=subtitle, 

435 email_template_name="account_deletion_recovered", 

436 email_template_args={ 

437 "app_link": urls.app_link(), 

438 }, 

439 push_title=title, 

440 push_body=subtitle, 

441 push_icon=urls.icon_url(), 

442 push_url=urls.app_link(), 

443 ) 

444 elif notification.topic_action.display == "chat:message": 

445 return RenderedNotification( 

446 email_subject=data.message, 

447 email_preview="You received a message on Couchers.org!", 

448 email_template_name="chat_message", 

449 email_template_args={ 

450 "author": data.author, 

451 "message": data.message, 

452 "text": data.text, 

453 "view_link": urls.chat_link(chat_id=data.group_chat_id), 

454 }, 

455 email_topic_action_unsubscribe_text="new chat messages", 

456 email_topic_key_unsubscribe_text="this chat (mute)", 

457 push_title=data.message, 

458 push_body=data.text, 

459 push_icon=v2avatar(data.author), 

460 push_url=urls.chat_link(chat_id=data.group_chat_id), 

461 ) 

462 elif notification.topic_action.display == "chat:missed_messages": 

463 return RenderedNotification( 

464 email_subject="You have unseen messages on Couchers.org!", 

465 email_preview="You missed some messages on the platform.", 

466 email_template_name="chat_unseen_messages", 

467 email_template_args={ 

468 "items": [ 

469 { 

470 "author": item.author, 

471 "message": item.message, 

472 "text": item.text, 

473 "view_link": urls.chat_link(chat_id=item.group_chat_id), 

474 } 

475 for item in data.messages 

476 ] 

477 }, 

478 email_topic_action_unsubscribe_text="unseen chat messages", 

479 push_title="You have unseen messages on Couchers.org", 

480 push_body="Please check out any messages you missed.", 

481 push_icon=urls.icon_url(), 

482 push_url=urls.messages_link(), 

483 ) 

484 elif notification.topic == "event": 

485 event = data.event 

486 time_display = f"{v2timestamp(event.start_time, user)} - {v2timestamp(event.end_time, user)}" 

487 event_link = urls.event_link(occurrence_id=event.event_id, slug=event.slug) 

488 if notification.action in ["create_approved", "create_any"]: 

489 # create_approved = invitation, approved by mods 

490 # create_any = new event created by anyone (no need for approval) -- off by default 

491 body = f"{time_display}\n" 

492 if notification.action == "create_approved": 

493 subject = f'{data.inviting_user.name} invited you to "{event.title}"' 

494 start_text = "You've been invited to a new event" 

495 body += f"Invited by {data.inviting_user.name}\n\n" 

496 elif notification.action == "create_any": 

497 subject = f'{data.inviting_user.name} created an event called "{event.title}"' 

498 start_text = "A new event was created" 

499 body += f"Created by {data.inviting_user.name}\n\n" 

500 body += event.content 

501 community_link = ( 

502 urls.community_link(node_id=data.in_community.community_id, slug=data.in_community.slug) 

503 if data.in_community 

504 else None 

505 ) 

506 return RenderedNotification( 

507 email_subject=subject, 

508 email_preview=f"{start_text} on Couchers.org!", 

509 email_template_name="event_create", 

510 email_template_args={ 

511 "inviting_user": data.inviting_user, 

512 "time_display": time_display, 

513 "start_text": start_text, 

514 "nearby": "nearby" if data.nearby else None, 

515 "community": data.in_community if data.in_community else None, 

516 "community_link": community_link, 

517 "event": event, 

518 "view_link": event_link, 

519 }, 

520 email_topic_action_unsubscribe_text=( 

521 "new events by community members" 

522 if notification.action == "create_any" 

523 else "invitations to events (approved by moderators)" 

524 ), 

525 push_title=subject, 

526 push_body=body, 

527 push_icon=v2avatar(data.inviting_user), 

528 push_url=event_link, 

529 ) 

530 elif notification.action == "update": 

531 updated_text = ", ".join(data.updated_items) 

532 body = f"{time_display}\n" 

533 body += f"{data.updating_user.name} updated: {updated_text}\n\n" 

534 body += event.content 

535 return RenderedNotification( 

536 email_subject=f'{data.updating_user.name} updated "{event.title}"', 

537 email_preview="An event you are subscribed to was updated.", 

538 email_template_name="event_update", 

539 email_template_args={ 

540 "updating_user": data.updating_user, 

541 "time_display": time_display, 

542 "event": event, 

543 "updated_text": updated_text, 

544 "view_link": event_link, 

545 }, 

546 email_topic_action_unsubscribe_text="event updates", 

547 push_title=f'{data.updating_user.name} updated "{event.title}"', 

548 push_body=body, 

549 push_icon=v2avatar(data.updating_user), 

550 push_url=event_link, 

551 ) 

552 elif notification.action == "cancel": 

553 body = f"{time_display}\n" 

554 body += f"The event has been cancelled by {data.cancelling_user.name}.\n\n" 

555 body += event.content 

556 return RenderedNotification( 

557 email_subject=f'{data.cancelling_user.name} cancelled "{event.title}"', 

558 email_preview="An event you are subscribed to has been cancelled.", 

559 email_template_name="event_cancel", 

560 email_template_args={ 

561 "cancelling_user": data.cancelling_user, 

562 "time_display": time_display, 

563 "event": event, 

564 "view_link": event_link, 

565 }, 

566 email_topic_action_unsubscribe_text="event cancellations", 

567 push_title=f'{data.cancelling_user.name} cancelled "{event.title}"', 

568 push_body=body, 

569 push_icon=v2avatar(data.cancelling_user), 

570 push_url=event_link, 

571 ) 

572 elif notification.action == "delete": 

573 return RenderedNotification( 

574 email_subject=f'A moderator deleted "{event.title}"', 

575 email_preview="An event you are subscribed to has been deleted.", 

576 email_template_name="event_delete", 

577 email_template_args={ 

578 "time_display": time_display, 

579 "event": event, 

580 }, 

581 email_topic_action_unsubscribe_text="event deletions", 

582 push_title=f'A moderator deleted "{event.title}"', 

583 push_body=f"{time_display}\nThe event has been deleted by the moderators.", 

584 push_icon=urls.icon_url(), 

585 push_url=urls.app_link(), 

586 ) 

587 elif notification.action == "invite_organizer": 

588 body = f"{time_display}\n" 

589 body += f"Invited to co-organize by {data.inviting_user.name}\n\n" 

590 body += event.content 

591 return RenderedNotification( 

592 email_subject=f'{data.inviting_user.name} invited you to co-organize "{event.title}"', 

593 email_preview="You were invited to co-organize an event on Couchers.org.", 

594 email_template_name="event_invite_organizer", 

595 email_template_args={ 

596 "inviting_user": data.inviting_user, 

597 "time_display": time_display, 

598 "event": event, 

599 "view_link": event_link, 

600 }, 

601 email_topic_action_unsubscribe_text="invitations to co-organize events", 

602 push_title=f'{data.inviting_user.name} invited you to co-organize "{event.title}"', 

603 push_body=body, 

604 push_icon=v2avatar(data.inviting_user), 

605 push_url=event_link, 

606 ) 

607 elif notification.action == "comment": 

608 body = f"{time_display}\n" 

609 body += f"{data.author.name} commented:\n\n" 

610 body += data.reply.content 

611 return RenderedNotification( 

612 email_subject=f'{data.author.name} commented on "{event.title}"', 

613 email_preview="Someone commented on an event you are attending.", 

614 email_template_name="event_comment", 

615 email_template_args={ 

616 "author": data.author, 

617 "time_display": time_display, 

618 "event": event, 

619 "content": data.reply.content, 

620 "view_link": event_link, 

621 }, 

622 email_topic_action_unsubscribe_text="event comments", 

623 push_title=f'{data.author.name} commented on "{event.title}"', 

624 push_body=body, 

625 push_icon=v2avatar(data.author), 

626 push_url=event_link, 

627 ) 

628 elif notification.topic == "discussion": 

629 discussion = data.discussion 

630 discussion_link = urls.discussion_link(discussion_id=discussion.discussion_id, slug=discussion.slug) 

631 if notification.action == "create": 

632 body = f"{data.author.name} created a discussion in {discussion.owner_title}: {discussion.title}\n\n" 

633 body += discussion.content 

634 return RenderedNotification( 

635 email_subject=f'{data.author.name} created a discussion: "{discussion.title}"', 

636 email_preview="Someone created a discussion in a community or group you are subscribed to.", 

637 email_template_name="discussion_create", 

638 email_template_args={ 

639 "author": data.author, 

640 "discussion": discussion, 

641 "view_link": discussion_link, 

642 }, 

643 email_topic_action_unsubscribe_text="new discussions", 

644 push_title=discussion.title, 

645 push_body=body, 

646 push_icon=v2avatar(data.author), 

647 push_url=discussion_link, 

648 ) 

649 elif notification.action == "comment": 

650 body = f"{data.author.name} commented:\n\n" 

651 body += data.reply.content 

652 return RenderedNotification( 

653 email_subject=f'{data.author.name} commented on "{discussion.title}"', 

654 email_preview="Someone commented on your discussion.", 

655 email_template_name="discussion_comment", 

656 email_template_args={ 

657 "author": data.author, 

658 "discussion": discussion, 

659 "reply": data.reply, 

660 "view_link": discussion_link, 

661 }, 

662 email_topic_action_unsubscribe_text="discussion comments", 

663 push_title=discussion.title, 

664 push_body=body, 

665 push_icon=v2avatar(data.author), 

666 push_url=discussion_link, 

667 ) 

668 elif notification.topic_action.display == "thread:reply": 

669 parent = data.WhichOneof("reply_parent") 

670 if parent == "event": 

671 title = data.event.title 

672 view_link = urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug) 

673 elif parent == "discussion": 

674 title = data.discussion.title 

675 view_link = urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug) 

676 else: 

677 raise Exception("Can only do replies to events and discussions") 

678 

679 body = f"{data.author.name} replied:\n\n" 

680 body += data.reply.content 

681 return RenderedNotification( 

682 email_subject=f'{data.author.name} replied in "{title}"', 

683 email_preview="Someone replied in a comment thread you have participated in.", 

684 email_template_name="comment_reply", 

685 email_template_args={ 

686 "author": data.author, 

687 "title": title, 

688 "reply": data.reply, 

689 "view_link": view_link, 

690 }, 

691 email_topic_action_unsubscribe_text="comment replies", 

692 push_title=title, 

693 push_body=body, 

694 push_icon=v2avatar(data.author), 

695 push_url=view_link, 

696 ) 

697 elif notification.topic == "reference": 

698 if notification.action == "receive_friend": 

699 title = f"You've received a friend reference from {data.from_user.name}!" 

700 return RenderedNotification( 

701 email_subject=title, 

702 email_preview=v2esc(data.text), 

703 email_template_name="friend_reference", 

704 email_template_args={ 

705 "from_user": data.from_user, 

706 "profile_references_link": urls.profile_references_link(), 

707 "text": data.text, 

708 }, 

709 email_topic_action_unsubscribe_text="new references from friends", 

710 push_title=title, 

711 push_body=data.text, 

712 push_icon=v2avatar(data.from_user), 

713 push_url=urls.profile_references_link(), 

714 ) 

715 elif notification.action in ["receive_hosted", "receive_surfed"]: 

716 title = f"You've received a reference from {data.from_user.name}!" 

717 # what was my type? i surfed with them if i received a "hosted" request 

718 surfed = notification.action == "receive_hosted" 

719 leave_reference_link = urls.leave_reference_link( 

720 reference_type="surfed" if surfed else "hosted", 

721 to_user_id=data.from_user.user_id, 

722 host_request_id=data.host_request_id, 

723 ) 

724 profile_references_link = urls.profile_references_link() 

725 if data.text: 

726 body = v2esc(data.text) 

727 push_url = profile_references_link 

728 else: 

729 body = "Please go and write a reference for them too. It's a nice gesture and helps us build a community together!" 

730 push_url = leave_reference_link 

731 return RenderedNotification( 

732 email_subject=title, 

733 email_preview=body, 

734 email_template_name="host_reference", 

735 email_template_args={ 

736 "from_user": data.from_user, 

737 "leave_reference_link": leave_reference_link, 

738 "profile_references_link": profile_references_link, 

739 "text": data.text, 

740 "both_written": True if data.text else False, 

741 "surfed": surfed, 

742 }, 

743 email_topic_action_unsubscribe_text="new references from " + ("hosts" if surfed else "surfers"), 

744 push_title=title, 

745 push_body=body, 

746 push_icon=v2avatar(data.from_user), 

747 push_url=push_url, 

748 ) 

749 elif notification.action in ["reminder_hosted", "reminder_surfed"]: 

750 # what was my type? i surfed with them if i get a surfed reminder 

751 surfed = notification.action == "reminder_surfed" 

752 leave_reference_link = urls.leave_reference_link( 

753 reference_type="surfed" if surfed else "hosted", 

754 to_user_id=data.other_user.user_id, 

755 host_request_id=data.host_request_id, 

756 ) 

757 title = f"You have {data.days_left} days to write a reference for {data.other_user.name}!" 

758 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." 

759 return RenderedNotification( 

760 email_subject=title, 

761 email_preview=preview, 

762 email_template_name="reference_reminder", 

763 email_template_args={ 

764 "other_user": data.other_user, 

765 "leave_reference_link": leave_reference_link, 

766 "days_left": str(data.days_left), 

767 "surfed": surfed, 

768 }, 

769 email_topic_action_unsubscribe_text=("surfed" if surfed else "hosted") + " reference reminders", 

770 push_title=title, 

771 push_body=preview, 

772 push_icon=v2avatar(data.other_user), 

773 push_url=leave_reference_link, 

774 ) 

775 elif notification.topic_action.display == "onboarding:reminder": 

776 if notification.key == "1": 

777 return RenderedNotification( 

778 email_subject="Welcome to Couchers.org and the future of couch surfing", 

779 email_preview="We are so excited to have you join our community!", 

780 email_template_name="onboarding1", 

781 email_template_args={ 

782 "app_link": urls.app_link(), 

783 "edit_profile_link": urls.edit_profile_link(), 

784 }, 

785 email_topic_action_unsubscribe_text="onboarding emails", 

786 push_title="Welcome to Couchers.org and the future of couch surfing", 

787 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!", 

788 push_icon=urls.icon_url(), 

789 push_url=urls.edit_profile_link(), 

790 ) 

791 elif notification.key == "2": 

792 return RenderedNotification( 

793 email_subject="Complete your profile on Couchers.org", 

794 email_preview="We would ask one big favour of you: please fill out your profile by adding a photo and some text.", 

795 email_template_name="onboarding2", 

796 email_template_args={ 

797 "edit_profile_link": urls.edit_profile_link(), 

798 }, 

799 email_topic_action_unsubscribe_text="onboarding emails", 

800 push_title="Please complete your profile on Couchers.org!", 

801 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.", 

802 push_icon=urls.icon_url(), 

803 push_url=urls.edit_profile_link(), 

804 ) 

805 elif notification.topic_action.display == "modnote:create": 

806 title = "You have received a mod note" 

807 message = "You have received an important note from the moderators. You must read and acknowledge it before continuing to use the platform." 

808 return RenderedNotification( 

809 is_critical=True, 

810 email_subject=title, 

811 email_preview=message, 

812 email_template_name="mod_note", 

813 email_template_args={"title": title}, 

814 push_title="You received a mod note", 

815 push_body="You need to read and acknowledge the note before continuing to use the platform.", 

816 push_icon=urls.icon_url(), 

817 push_url=urls.app_link(), 

818 ) 

819 elif notification.topic_action.display == "verification:sv_success": 

820 title = "Strong Verification succeeded" 

821 message = "You have been verified with Strong Verification! You will now see a tick next to your name on the platform." 

822 return RenderedNotification( 

823 is_critical=True, 

824 email_subject=title, 

825 email_preview=title, 

826 email_template_name="security", 

827 email_template_args={ 

828 "title": title, 

829 "message": message, 

830 }, 

831 push_title=title, 

832 push_body=message, 

833 push_icon=urls.icon_url(), 

834 push_url=urls.account_settings_link(), 

835 ) 

836 elif notification.topic_action.display == "verification:sv_fail": 

837 title = "Strong Verification failed" 

838 message: str 

839 if data.reason == notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER: 

840 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." 

841 elif data.reason == notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT: 

842 message = "You tried to verify with a document that is not a passport. You can only use a passport for Strong Verification." 

843 elif data.reason == notification_data_pb2.SV_FAIL_REASON_DUPLICATE: 

844 message = "You tried to verify with a passport that has already been used for verification. Please use another passport." 

845 else: 

846 raise Exception("Shouldn't get here") 

847 return RenderedNotification( 

848 is_critical=True, 

849 email_subject=title, 

850 email_preview=title, 

851 email_template_name="security", 

852 email_template_args={ 

853 "title": title, 

854 "message": message, 

855 }, 

856 push_title=title, 

857 push_body=message, 

858 push_icon=urls.icon_url(), 

859 push_url=urls.account_settings_link(), 

860 ) 

861 elif notification.topic_action.display == "activeness:probe": 

862 title = "Are you still open to hosting on Couchers.org?" 

863 return RenderedNotification( 

864 email_subject=title, 

865 email_preview=title, 

866 email_template_name="activeness_probe", 

867 email_template_args={ 

868 "app_link": urls.app_link(), 

869 "days_left": (to_aware_datetime(data.deadline) - now()).days, 

870 }, 

871 push_title=title, 

872 push_body="Please log in to confirm your hosting status.", 

873 push_icon=urls.icon_url(), 

874 push_url=urls.app_link(), 

875 ) 

876 elif notification.topic_action.display == "general:new_blog_post": 

877 title = f"New blog post: {data.title}" 

878 return RenderedNotification( 

879 email_subject=title, 

880 email_preview=data.blurb, 

881 email_template_name="new_blog_post", 

882 email_template_args={ 

883 "title": data.title, 

884 "blurb": data.blurb, 

885 "url": data.url, 

886 }, 

887 email_topic_action_unsubscribe_text="new blog post alerts", 

888 push_title=title, 

889 push_body=data.blurb, 

890 push_icon=urls.icon_url(), 

891 push_url=data.url, 

892 ) 

893 else: 

894 raise NotImplementedError(f"Unknown topic-action: {notification.topic}:{notification.action}")