Coverage for app / backend / src / couchers / notifications / render_push.py: 87%

289 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-05 09:44 +0000

1""" 

2Renders a Notification model into a localized push notification. 

3""" 

4 

5import logging 

6from datetime import date 

7from typing import Any, assert_never 

8 

9from couchers import urls 

10from couchers.i18n import LocalizationContext 

11from couchers.i18n.i18next import LocalizationError 

12from couchers.i18n.localize import format_phone_number 

13from couchers.models import Notification, NotificationTopicAction 

14from couchers.notifications.locales import get_notifs_i18next 

15from couchers.notifications.push import PushNotificationContent 

16from couchers.proto import api_pb2, notification_data_pb2 

17 

18logger = logging.getLogger(__name__) 

19 

20# See PushNotificationContent's documentation for notification writing guidelines. 

21 

22 

23def render_push_notification(notification: Notification, loc_context: LocalizationContext) -> PushNotificationContent: 

24 data: Any = notification.topic_action.data_type.FromString(notification.data) # type: ignore[attr-defined] 

25 

26 match notification.topic_action: 

27 # Using a match statement enable mypy's exhaustiveness checking. 

28 # Every case is has its own function so that they can declare different types for "data", 

29 # as mypy wouldn't allow that in a single function. 

30 # Keep topics sorted (actions can follow logical ordering) 

31 case NotificationTopicAction.account_deletion__start: 

32 return _render_account_deletion__start(data, loc_context) 

33 case NotificationTopicAction.account_deletion__complete: 

34 return _render_account_deletion__complete(data, loc_context) 

35 case NotificationTopicAction.account_deletion__recovered: 

36 return _render_account_deletion__recovered(loc_context) 

37 case NotificationTopicAction.activeness__probe: 

38 return _render_activeness__probe(data, loc_context) 

39 case NotificationTopicAction.api_key__create: 

40 return _render_api_key__create(data, loc_context) 

41 case NotificationTopicAction.badge__add: 

42 return _render_badge__add(data, loc_context) 

43 case NotificationTopicAction.badge__remove: 

44 return _render_badge__remove(data, loc_context) 

45 case NotificationTopicAction.birthdate__change: 

46 return _render_birthdate__change(data, loc_context) 

47 case NotificationTopicAction.chat__message: 

48 return _render_chat__message(data, loc_context) 

49 case NotificationTopicAction.chat__missed_messages: 49 ↛ 50line 49 didn't jump to line 50 because the pattern on line 49 never matched

50 return _render_chat__missed_messages(data, loc_context) 

51 case NotificationTopicAction.donation__received: 

52 return _render_donation__received(data, loc_context) 

53 case NotificationTopicAction.discussion__create: 

54 return _render_discussion__create(data, loc_context) 

55 case NotificationTopicAction.discussion__comment: 

56 return _render_discussion__comment(data, loc_context) 

57 case NotificationTopicAction.email_address__change: 

58 return _render_email_address__change(data, loc_context) 

59 case NotificationTopicAction.email_address__verify: 

60 return _render_email_address__verify(loc_context) 

61 case NotificationTopicAction.event__create_any: 61 ↛ 62line 61 didn't jump to line 62 because the pattern on line 61 never matched

62 return _render_event__create_any(data, loc_context) 

63 case NotificationTopicAction.event__create_approved: 

64 return _render_event__create_approved(data, loc_context) 

65 case NotificationTopicAction.event__update: 

66 return _render_event__update(data, loc_context) 

67 case NotificationTopicAction.event__invite_organizer: 67 ↛ 68line 67 didn't jump to line 68 because the pattern on line 67 never matched

68 return _render_event__invite_organizer(data, loc_context) 

69 case NotificationTopicAction.event__comment: 

70 return _render_event__comment(data, loc_context) 

71 case NotificationTopicAction.event__reminder: 

72 return _render_event__reminder(data, loc_context) 

73 case NotificationTopicAction.event__cancel: 

74 return _render_event__cancel(data, loc_context) 

75 case NotificationTopicAction.event__delete: 75 ↛ 76line 75 didn't jump to line 76 because the pattern on line 75 never matched

76 return _render_event__delete(data, loc_context) 

77 case NotificationTopicAction.friend_request__create: 

78 return _render_friend_request__create(data, loc_context) 

79 case NotificationTopicAction.friend_request__accept: 

80 return _render_friend_request__accept(data, loc_context) 

81 case NotificationTopicAction.gender__change: 

82 return _render_gender__change(data, loc_context) 

83 case NotificationTopicAction.general__new_blog_post: 

84 return _render_general__new_blog_post(data, loc_context) 

85 case NotificationTopicAction.host_request__create: 

86 return _render_host_request__create(data, loc_context) 

87 case NotificationTopicAction.host_request__message: 

88 return _render_host_request__message(data, loc_context) 

89 case NotificationTopicAction.host_request__missed_messages: 89 ↛ 90line 89 didn't jump to line 90 because the pattern on line 89 never matched

90 return _render_host_request__missed_messages(data, loc_context) 

91 case NotificationTopicAction.host_request__reminder: 

92 return _render_host_request__reminder(data, loc_context) 

93 case NotificationTopicAction.host_request__accept: 

94 return _render_host_request__accept(data, loc_context) 

95 case NotificationTopicAction.host_request__reject: 95 ↛ 96line 95 didn't jump to line 96 because the pattern on line 95 never matched

96 return _render_host_request__reject(data, loc_context) 

97 case NotificationTopicAction.host_request__cancel: 97 ↛ 98line 97 didn't jump to line 98 because the pattern on line 97 never matched

98 return _render_host_request__cancel(data, loc_context) 

99 case NotificationTopicAction.host_request__confirm: 

100 return _render_host_request__confirm(data, loc_context) 

101 case NotificationTopicAction.modnote__create: 

102 return _render_modnote__create(loc_context) 

103 case NotificationTopicAction.onboarding__reminder: 

104 return _render_onboarding__reminder(notification.key, loc_context) 

105 case NotificationTopicAction.password__change: 

106 return _render_password__change(loc_context) 

107 case NotificationTopicAction.password_reset__start: 

108 return _render_password_reset__start(data, loc_context) 

109 case NotificationTopicAction.password_reset__complete: 

110 return _render_password_reset__complete(loc_context) 

111 case NotificationTopicAction.phone_number__change: 

112 return _render_phone_number__change(data, loc_context) 

113 case NotificationTopicAction.phone_number__verify: 

114 return _render_phone_number__verify(data, loc_context) 

115 case NotificationTopicAction.postal_verification__postcard_sent: 

116 return _render_postal_verification__postcard_sent(data, loc_context) 

117 case NotificationTopicAction.postal_verification__success: 117 ↛ 118line 117 didn't jump to line 118 because the pattern on line 117 never matched

118 return _render_postal_verification__success(loc_context) 

119 case NotificationTopicAction.postal_verification__failed: 119 ↛ 120line 119 didn't jump to line 120 because the pattern on line 119 never matched

120 return _render_postal_verification__failed(data, loc_context) 

121 case NotificationTopicAction.reference__receive_friend: 

122 return _render_reference__receive_friend(data, loc_context) 

123 case NotificationTopicAction.reference__receive_hosted: 123 ↛ 124line 123 didn't jump to line 124 because the pattern on line 123 never matched

124 return _render_reference__receive_hosted(data, loc_context) 

125 case NotificationTopicAction.reference__receive_surfed: 

126 return _render_reference__receive_surfed(data, loc_context) 

127 case NotificationTopicAction.reference__reminder_hosted: 

128 return _render_reference__reminder_hosted(data, loc_context) 

129 case NotificationTopicAction.reference__reminder_surfed: 

130 return _render_reference__reminder_surfed(data, loc_context) 

131 case NotificationTopicAction.thread__reply: 

132 return _render_thread__reply(data, loc_context) 

133 case NotificationTopicAction.verification__sv_success: 

134 return _render_verification__sv_success(loc_context) 

135 case NotificationTopicAction.verification__sv_fail: 135 ↛ 137line 135 didn't jump to line 137 because the pattern on line 135 always matched

136 return _render_verification__sv_fail(data, loc_context) 

137 case _: 

138 # Enables mypy's exhaustiveness checking for the cases above. 

139 assert_never(notification.topic_action) 

140 

141 

142def render_adhoc_push_notification(name: str, loc_context: LocalizationContext) -> PushNotificationContent: 

143 """Renders a push notification that doesn't have an assigned topic-action.""" 

144 return _get_content(string_group=f"_adhoc.{name}.push", loc_context=loc_context) 

145 

146 

147def _get_content( 

148 string_group: NotificationTopicAction | str, 

149 loc_context: LocalizationContext, 

150 title: str | None = None, 

151 ios_title: str | None = None, 

152 ios_subtitle: str | None = None, 

153 body: str | None = None, 

154 substitutions: dict[str, str | int] | None = None, 

155 icon_user: api_pb2.User | None = None, 

156 action_url: str | None = None, 

157) -> PushNotificationContent: 

158 """ 

159 Fills a PushNotificationContent by looking up localized 

160 string based on the topic_action key, unless other strings 

161 are provided by the caller. 

162 

163 Localized strings have the provided substitutions applied. 

164 """ 

165 # Look up the localized string for any string that was not provided 

166 if title is None: 166 ↛ 168line 166 didn't jump to line 168 because the condition on line 166 was always true

167 title = _get_string(string_group, "title", loc_context, substitutions) 

168 if ios_title is None: 

169 ios_title = _get_string(string_group, "ios_title", loc_context, substitutions) 

170 if ios_subtitle is None: 

171 try: 

172 ios_subtitle = _get_string(string_group, "ios_subtitle", loc_context, substitutions) 

173 except LocalizationError: 

174 # Not all notifications have subtitles 

175 pass 

176 if body is None: 

177 body = _get_string(string_group, "body", loc_context, substitutions) 

178 

179 icon_url = _avatar_url_or_default(icon_user) if icon_user else None 

180 

181 return PushNotificationContent( 

182 title=title, ios_title=title, ios_subtitle=ios_subtitle, body=body, icon_url=icon_url, action_url=action_url 

183 ) 

184 

185 

186def _get_string( 

187 string_group: NotificationTopicAction | str, 

188 key: str, 

189 loc_context: LocalizationContext, 

190 substitutions: dict[str, str | int] | None = None, 

191) -> str: 

192 if isinstance(string_group, NotificationTopicAction): 

193 full_key = f"{string_group.topic}.{string_group.action}.push.{key}" 

194 else: 

195 full_key = f"{string_group}.{key}" 

196 return get_notifs_i18next().localize(full_key, loc_context.locale, substitutions) 

197 

198 

199def _avatar_url_or_default(user: api_pb2.User) -> str: 

200 return user.avatar_thumbnail_url or urls.icon_url() 

201 

202 

203def _render_account_deletion__start( 

204 data: notification_data_pb2.AccountDeletionStart, loc_context: LocalizationContext 

205) -> PushNotificationContent: 

206 return _get_content(NotificationTopicAction.account_deletion__start, loc_context) 

207 

208 

209def _render_account_deletion__complete( 

210 data: notification_data_pb2.AccountDeletionComplete, loc_context: LocalizationContext 

211) -> PushNotificationContent: 

212 return _get_content( 

213 NotificationTopicAction.account_deletion__complete, loc_context, substitutions={"count": data.undelete_days} 

214 ) 

215 

216 

217def _render_account_deletion__recovered(loc_context: LocalizationContext) -> PushNotificationContent: 

218 return _get_content(NotificationTopicAction.account_deletion__recovered, loc_context) 

219 

220 

221def _render_activeness__probe( 

222 data: notification_data_pb2.ActivenessProbe, loc_context: LocalizationContext 

223) -> PushNotificationContent: 

224 return _get_content(NotificationTopicAction.activeness__probe, loc_context) 

225 

226 

227def _render_api_key__create( 

228 data: notification_data_pb2.ApiKeyCreate, loc_context: LocalizationContext 

229) -> PushNotificationContent: 

230 return _get_content(NotificationTopicAction.api_key__create, loc_context) 

231 

232 

233def _render_badge__add( 

234 data: notification_data_pb2.BadgeAdd, loc_context: LocalizationContext 

235) -> PushNotificationContent: 

236 return _get_content( 

237 NotificationTopicAction.badge__add, 

238 loc_context, 

239 substitutions={"badge_name": data.badge_name}, 

240 action_url=urls.profile_link(), 

241 ) 

242 

243 

244def _render_badge__remove( 

245 data: notification_data_pb2.BadgeRemove, loc_context: LocalizationContext 

246) -> PushNotificationContent: 

247 return _get_content( 

248 NotificationTopicAction.badge__remove, 

249 loc_context, 

250 substitutions={"badge_name": data.badge_name}, 

251 action_url=urls.profile_link(), 

252 ) 

253 

254 

255def _render_birthdate__change( 

256 data: notification_data_pb2.BirthdateChange, loc_context: LocalizationContext 

257) -> PushNotificationContent: 

258 return _get_content( 

259 NotificationTopicAction.birthdate__change, 

260 loc_context, 

261 substitutions={"birthdate": loc_context.localize_date_from_iso(data.birthdate)}, 

262 action_url=urls.account_settings_link(), 

263 ) 

264 

265 

266def _render_chat__message( 

267 data: notification_data_pb2.ChatMessage, loc_context: LocalizationContext 

268) -> PushNotificationContent: 

269 # All strings are dynamic, no need to use _get_content 

270 return PushNotificationContent( 

271 title=data.author.name, 

272 ios_title=data.author.name, 

273 body=data.text, 

274 icon_url=_avatar_url_or_default(data.author), 

275 action_url=urls.chat_link(chat_id=data.group_chat_id), 

276 ) 

277 

278 

279def _render_chat__missed_messages( 

280 data: notification_data_pb2.ChatMissedMessages, loc_context: LocalizationContext 

281) -> PushNotificationContent: 

282 return _get_content( 

283 NotificationTopicAction.chat__missed_messages, 

284 loc_context, 

285 substitutions={"count": len(data.messages)}, 

286 action_url=urls.messages_link(), 

287 ) 

288 

289 

290def _render_donation__received( 

291 data: notification_data_pb2.DonationReceived, loc_context: LocalizationContext 

292) -> PushNotificationContent: 

293 return _get_content( 

294 NotificationTopicAction.donation__received, 

295 loc_context, 

296 # Other currencies are not yet supported 

297 substitutions={"amount_with_currency": f"${data.amount}"}, 

298 action_url=data.receipt_url, 

299 ) 

300 

301 

302def _render_discussion__create( 

303 data: notification_data_pb2.DiscussionCreate, loc_context: LocalizationContext 

304) -> PushNotificationContent: 

305 return _get_content( 

306 NotificationTopicAction.discussion__create, 

307 loc_context, 

308 substitutions={ 

309 "title": data.discussion.title, 

310 "user": data.author.name, 

311 "group_or_community": data.discussion.owner_title, 

312 }, 

313 icon_user=data.author, 

314 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug), 

315 ) 

316 

317 

318def _render_discussion__comment( 

319 data: notification_data_pb2.DiscussionComment, loc_context: LocalizationContext 

320) -> PushNotificationContent: 

321 return _get_content( 

322 NotificationTopicAction.discussion__comment, 

323 loc_context, 

324 ios_title=data.author.name, 

325 ios_subtitle=data.discussion.title, 

326 body=data.reply.content, 

327 substitutions={"user": data.author.name, "title": data.discussion.title}, 

328 icon_user=data.author, 

329 action_url=urls.discussion_link(discussion_id=data.discussion.discussion_id, slug=data.discussion.slug), 

330 ) 

331 

332 

333def _render_email_address__change( 

334 data: notification_data_pb2.EmailAddressChange, loc_context: LocalizationContext 

335) -> PushNotificationContent: 

336 return _get_content( 

337 NotificationTopicAction.email_address__change, 

338 loc_context, 

339 substitutions={"email": data.new_email}, 

340 action_url=urls.account_settings_link(), 

341 ) 

342 

343 

344def _render_email_address__verify(loc_context: LocalizationContext) -> PushNotificationContent: 

345 return _get_content( 

346 NotificationTopicAction.email_address__verify, 

347 loc_context, 

348 action_url=urls.account_settings_link(), 

349 ) 

350 

351 

352def _render_event__create_any( 

353 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext 

354) -> PushNotificationContent: 

355 return _get_content( 

356 NotificationTopicAction.event__create_any, 

357 loc_context, 

358 substitutions={ 

359 "title": data.event.title, 

360 "user": data.inviting_user.name, 

361 "date_and_time": loc_context.localize_datetime(data.event.start_time), 

362 }, 

363 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug), 

364 ) 

365 

366 

367def _render_event__create_approved( 

368 data: notification_data_pb2.EventCreate, loc_context: LocalizationContext 

369) -> PushNotificationContent: 

370 return _get_content( 

371 NotificationTopicAction.event__create_approved, 

372 loc_context, 

373 substitutions={ 

374 "title": data.event.title, 

375 "user": data.inviting_user.name, 

376 "date_and_time": loc_context.localize_datetime(data.event.start_time), 

377 }, 

378 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug), 

379 ) 

380 

381 

382def _render_event__update( 

383 data: notification_data_pb2.EventUpdate, loc_context: LocalizationContext 

384) -> PushNotificationContent: 

385 # updated_items can include: title, content, start_time, end_time, location, 

386 # but a list like that is tricky to localize. 

387 return _get_content( 

388 NotificationTopicAction.event__update, 

389 loc_context, 

390 substitutions={ 

391 "title": data.event.title, 

392 "user": data.updating_user.name, 

393 }, 

394 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug), 

395 ) 

396 

397 

398def _render_event__invite_organizer( 

399 data: notification_data_pb2.EventInviteOrganizer, loc_context: LocalizationContext 

400) -> PushNotificationContent: 

401 return _get_content( 

402 NotificationTopicAction.event__invite_organizer, 

403 loc_context, 

404 substitutions={ 

405 "title": data.event.title, 

406 "user": data.inviting_user.name, 

407 }, 

408 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug), 

409 ) 

410 

411 

412def _render_event__comment( 

413 data: notification_data_pb2.EventComment, loc_context: LocalizationContext 

414) -> PushNotificationContent: 

415 return _get_content( 

416 NotificationTopicAction.event__comment, 

417 loc_context, 

418 substitutions={ 

419 "title": data.event.title, 

420 "user": data.author.name, 

421 }, 

422 body=data.reply.content, 

423 icon_user=data.author, 

424 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug), 

425 ) 

426 

427 

428def _render_event__reminder( 

429 data: notification_data_pb2.EventReminder, loc_context: LocalizationContext 

430) -> PushNotificationContent: 

431 return _get_content( 

432 NotificationTopicAction.event__reminder, 

433 loc_context, 

434 substitutions={ 

435 "title": data.event.title, 

436 "date_and_time": loc_context.localize_datetime(data.event.start_time), 

437 }, 

438 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug), 

439 ) 

440 

441 

442def _render_event__cancel( 

443 data: notification_data_pb2.EventCancel, loc_context: LocalizationContext 

444) -> PushNotificationContent: 

445 return _get_content( 

446 NotificationTopicAction.event__cancel, 

447 loc_context, 

448 substitutions={ 

449 "title": data.event.title, 

450 "user": data.cancelling_user.name, 

451 }, 

452 action_url=urls.event_link(occurrence_id=data.event.event_id, slug=data.event.slug), 

453 ) 

454 

455 

456def _render_event__delete( 

457 data: notification_data_pb2.EventDelete, loc_context: LocalizationContext 

458) -> PushNotificationContent: 

459 return _get_content(NotificationTopicAction.event__delete, loc_context, substitutions={"title": data.event.title}) 

460 

461 

462def _render_friend_request__create( 

463 data: notification_data_pb2.FriendRequestCreate, loc_context: LocalizationContext 

464) -> PushNotificationContent: 

465 return _get_content( 

466 NotificationTopicAction.friend_request__create, 

467 loc_context, 

468 substitutions={"from_user": data.other_user.name}, 

469 icon_user=data.other_user, 

470 action_url=urls.friend_requests_link(), 

471 ) 

472 

473 

474def _render_friend_request__accept( 

475 data: notification_data_pb2.FriendRequestAccept, loc_context: LocalizationContext 

476) -> PushNotificationContent: 

477 return _get_content( 

478 NotificationTopicAction.friend_request__accept, 

479 loc_context, 

480 substitutions={"friend": data.other_user.name}, 

481 icon_user=data.other_user, 

482 action_url=urls.user_link(username=data.other_user.username), 

483 ) 

484 

485 

486def _render_gender__change( 

487 data: notification_data_pb2.GenderChange, loc_context: LocalizationContext 

488) -> PushNotificationContent: 

489 return _get_content( 

490 NotificationTopicAction.gender__change, 

491 loc_context, 

492 substitutions={"gender": data.gender}, 

493 action_url=urls.account_settings_link(), 

494 ) 

495 

496 

497def _render_general__new_blog_post( 

498 data: notification_data_pb2.GeneralNewBlogPost, loc_context: LocalizationContext 

499) -> PushNotificationContent: 

500 return _get_content( 

501 NotificationTopicAction.general__new_blog_post, 

502 loc_context, 

503 body=data.blurb, 

504 substitutions={"title": data.title}, 

505 action_url=data.url, 

506 ) 

507 

508 

509def _render_host_request__create( 

510 data: notification_data_pb2.HostRequestCreate, loc_context: LocalizationContext 

511) -> PushNotificationContent: 

512 days = (date.fromisoformat(data.host_request.to_date) - date.fromisoformat(data.host_request.from_date)).days + 1 

513 return _get_content( 

514 NotificationTopicAction.host_request__create, 

515 loc_context, 

516 substitutions={ 

517 "user": data.surfer.name, 

518 "start_date": loc_context.localize_date_from_iso(data.host_request.from_date), 

519 "count": days, 

520 }, 

521 icon_user=data.surfer, 

522 action_url=urls.host_request(host_request_id=data.host_request.host_request_id), 

523 ) 

524 

525 

526def _render_host_request__message( 

527 data: notification_data_pb2.HostRequestMessage, loc_context: LocalizationContext 

528) -> PushNotificationContent: 

529 # All strings are dynamic, no need to use _get_content 

530 return PushNotificationContent( 

531 title=data.user.name, 

532 ios_title=data.user.name, 

533 body=data.text, 

534 action_url=urls.host_request(host_request_id=data.host_request.host_request_id), 

535 icon_url=_avatar_url_or_default(data.user), 

536 ) 

537 

538 

539def _render_host_request__missed_messages( 

540 data: notification_data_pb2.HostRequestMissedMessages, loc_context: LocalizationContext 

541) -> PushNotificationContent: 

542 return _get_content( 

543 NotificationTopicAction.host_request__missed_messages, 

544 loc_context, 

545 substitutions={"user": data.user.name}, 

546 icon_user=data.user, 

547 action_url=urls.host_request(host_request_id=data.host_request.host_request_id), 

548 ) 

549 

550 

551def _render_host_request__reminder( 

552 data: notification_data_pb2.HostRequestReminder, loc_context: LocalizationContext 

553) -> PushNotificationContent: 

554 return _get_content( 

555 NotificationTopicAction.host_request__reminder, 

556 loc_context, 

557 substitutions={"user": data.surfer.name}, 

558 icon_user=data.surfer, 

559 action_url=urls.host_request(host_request_id=data.host_request.host_request_id), 

560 ) 

561 

562 

563def _render_host_request__accept( 

564 data: notification_data_pb2.HostRequestAccept, loc_context: LocalizationContext 

565) -> PushNotificationContent: 

566 return _get_content( 

567 NotificationTopicAction.host_request__accept, 

568 loc_context, 

569 substitutions={ 

570 "user": data.host.name, 

571 "date": loc_context.localize_date_from_iso(data.host_request.from_date), 

572 }, 

573 icon_user=data.host, 

574 action_url=urls.host_request(host_request_id=data.host_request.host_request_id), 

575 ) 

576 

577 

578def _render_host_request__reject( 

579 data: notification_data_pb2.HostRequestReject, loc_context: LocalizationContext 

580) -> PushNotificationContent: 

581 return _get_content( 

582 NotificationTopicAction.host_request__reject, 

583 loc_context, 

584 substitutions={ 

585 "user": data.host.name, 

586 "date": loc_context.localize_date_from_iso(data.host_request.from_date), 

587 }, 

588 icon_user=data.host, 

589 action_url=urls.host_request(host_request_id=data.host_request.host_request_id), 

590 ) 

591 

592 

593def _render_host_request__cancel( 

594 data: notification_data_pb2.HostRequestCancel, loc_context: LocalizationContext 

595) -> PushNotificationContent: 

596 return _get_content( 

597 NotificationTopicAction.host_request__cancel, 

598 loc_context, 

599 substitutions={ 

600 "user": data.surfer.name, 

601 "date": loc_context.localize_date_from_iso(data.host_request.from_date), 

602 }, 

603 icon_user=data.surfer, 

604 action_url=urls.host_request(host_request_id=data.host_request.host_request_id), 

605 ) 

606 

607 

608def _render_host_request__confirm( 

609 data: notification_data_pb2.HostRequestConfirm, loc_context: LocalizationContext 

610) -> PushNotificationContent: 

611 return _get_content( 

612 NotificationTopicAction.host_request__confirm, 

613 loc_context, 

614 substitutions={ 

615 "user": data.surfer.name, 

616 "date": loc_context.localize_date_from_iso(data.host_request.from_date), 

617 }, 

618 icon_user=data.surfer, 

619 action_url=urls.host_request(host_request_id=data.host_request.host_request_id), 

620 ) 

621 

622 

623def _render_modnote__create(loc_context: LocalizationContext) -> PushNotificationContent: 

624 return _get_content(NotificationTopicAction.modnote__create, loc_context) 

625 

626 

627def _render_onboarding__reminder(key: str, loc_context: LocalizationContext) -> PushNotificationContent: 

628 variant = "first" if key == "1" else "subsequent" 

629 return _get_content( 

630 f"onboarding.reminder.push.{variant}", 

631 loc_context, 

632 action_url=urls.edit_profile_link(), 

633 ) 

634 

635 

636def _render_password__change(loc_context: LocalizationContext) -> PushNotificationContent: 

637 return _get_content(NotificationTopicAction.password__change, loc_context, action_url=urls.account_settings_link()) 

638 

639 

640def _render_password_reset__start( 

641 data: notification_data_pb2.PasswordResetStart, loc_context: LocalizationContext 

642) -> PushNotificationContent: 

643 return _get_content( 

644 NotificationTopicAction.password_reset__start, 

645 loc_context, 

646 action_url=urls.account_settings_link(), 

647 ) 

648 

649 

650def _render_password_reset__complete(loc_context: LocalizationContext) -> PushNotificationContent: 

651 return _get_content( 

652 NotificationTopicAction.password_reset__complete, 

653 loc_context, 

654 action_url=urls.account_settings_link(), 

655 ) 

656 

657 

658def _render_phone_number__change( 

659 data: notification_data_pb2.PhoneNumberChange, loc_context: LocalizationContext 

660) -> PushNotificationContent: 

661 return _get_content( 

662 NotificationTopicAction.phone_number__change, 

663 loc_context, 

664 substitutions={"phone_number": format_phone_number(data.phone)}, 

665 action_url=urls.account_settings_link(), 

666 ) 

667 

668 

669def _render_phone_number__verify( 

670 data: notification_data_pb2.PhoneNumberVerify, loc_context: LocalizationContext 

671) -> PushNotificationContent: 

672 return _get_content( 

673 NotificationTopicAction.phone_number__verify, 

674 loc_context, 

675 substitutions={"phone_number": format_phone_number(data.phone)}, 

676 action_url=urls.account_settings_link(), 

677 ) 

678 

679 

680def _render_postal_verification__postcard_sent( 

681 data: notification_data_pb2.PostalVerificationPostcardSent, loc_context: LocalizationContext 

682) -> PushNotificationContent: 

683 return _get_content( 

684 NotificationTopicAction.postal_verification__postcard_sent, 

685 loc_context, 

686 substitutions={"city": data.city, "country": data.country}, 

687 action_url=urls.account_settings_link(), 

688 ) 

689 

690 

691def _render_postal_verification__success(loc_context: LocalizationContext) -> PushNotificationContent: 

692 return _get_content( 

693 NotificationTopicAction.postal_verification__success, 

694 loc_context, 

695 action_url=urls.account_settings_link(), 

696 ) 

697 

698 

699def _render_postal_verification__failed( 

700 data: notification_data_pb2.PostalVerificationFailed, loc_context: LocalizationContext 

701) -> PushNotificationContent: 

702 body_key: str 

703 match data.reason: 

704 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_CODE_EXPIRED: 

705 body_key = "body_code_expired" 

706 case notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_TOO_MANY_ATTEMPTS: 

707 body_key = "body_too_many_attempts" 

708 case _: 

709 body_key = "body_generic" 

710 

711 return _get_content( 

712 NotificationTopicAction.postal_verification__failed, 

713 loc_context, 

714 body=_get_string(NotificationTopicAction.postal_verification__failed, body_key, loc_context), 

715 action_url=urls.account_settings_link(), 

716 ) 

717 

718 

719def _render_reference__receive_friend( 

720 data: notification_data_pb2.ReferenceReceiveFriend, loc_context: LocalizationContext 

721) -> PushNotificationContent: 

722 return _get_content( 

723 NotificationTopicAction.reference__receive_friend, 

724 loc_context, 

725 body=data.text, 

726 substitutions={"user": data.from_user.name}, 

727 icon_user=data.from_user, 

728 action_url=urls.profile_references_link(), 

729 ) 

730 

731 

732def _render_reference__receive( 

733 data: notification_data_pb2.ReferenceReceiveHostRequest, leave_reference_type: str, loc_context: LocalizationContext 

734) -> PushNotificationContent: 

735 body: str 

736 if data.text: 736 ↛ 737line 736 didn't jump to line 737 because the condition on line 736 was never true

737 body = data.text 

738 action_url = urls.profile_references_link() 

739 else: 

740 body = _get_string( 

741 "reference._receive_any.push", 

742 "body_must_write_yours", 

743 loc_context, 

744 substitutions={"user": data.from_user.name}, 

745 ) 

746 action_url = urls.leave_reference_link( 

747 reference_type=leave_reference_type, 

748 to_user_id=data.from_user.user_id, 

749 host_request_id=str(data.host_request_id), 

750 ) 

751 return _get_content( 

752 string_group="reference._receive_any.push", 

753 loc_context=loc_context, 

754 body=body, 

755 substitutions={"user": data.from_user.name}, 

756 icon_user=data.from_user, 

757 action_url=action_url, 

758 ) 

759 

760 

761def _render_reference__receive_hosted( 

762 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext 

763) -> PushNotificationContent: 

764 # Receiving a hosted reminder means I need to leave a surfed reference 

765 return _render_reference__receive(data, leave_reference_type="surfed", loc_context=loc_context) 

766 

767 

768def _render_reference__receive_surfed( 

769 data: notification_data_pb2.ReferenceReceiveHostRequest, loc_context: LocalizationContext 

770) -> PushNotificationContent: 

771 return _render_reference__receive(data, leave_reference_type="hosted", loc_context=loc_context) 

772 

773 

774def _render_reference__reminder( 

775 data: notification_data_pb2.ReferenceReminder, leave_reference_type: str, loc_context: LocalizationContext 

776) -> PushNotificationContent: 

777 leave_reference_link = urls.leave_reference_link( 

778 reference_type=leave_reference_type, 

779 to_user_id=data.other_user.user_id, 

780 host_request_id=str(data.host_request_id), 

781 ) 

782 return _get_content( 

783 string_group="reference._reminder_any.push", 

784 loc_context=loc_context, 

785 substitutions={"count": data.days_left, "user": data.other_user.name}, 

786 icon_user=data.other_user, 

787 action_url=leave_reference_link, 

788 ) 

789 

790 

791def _render_reference__reminder_surfed( 

792 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext 

793) -> PushNotificationContent: 

794 # Surfed reminder means I need to leave a surfed reference 

795 return _render_reference__reminder(data, leave_reference_type="surfed", loc_context=loc_context) 

796 

797 

798def _render_reference__reminder_hosted( 

799 data: notification_data_pb2.ReferenceReminder, loc_context: LocalizationContext 

800) -> PushNotificationContent: 

801 return _render_reference__reminder(data, leave_reference_type="hosted", loc_context=loc_context) 

802 

803 

804def _render_thread__reply( 

805 data: notification_data_pb2.ThreadReply, loc_context: LocalizationContext 

806) -> PushNotificationContent: 

807 parent_title: str 

808 view_link: str 

809 match data.WhichOneof("reply_parent"): 

810 case "event": 

811 parent_title = data.event.title 

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

813 case "discussion": 813 ↛ 816line 813 didn't jump to line 816 because the pattern on line 813 always matched

814 parent_title = data.discussion.title 

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

816 case _: 

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

818 

819 return _get_content( 

820 NotificationTopicAction.thread__reply, 

821 loc_context=loc_context, 

822 body=data.reply.content, 

823 substitutions={"user": data.author.name, "title": parent_title}, 

824 icon_user=data.author, 

825 action_url=view_link, 

826 ) 

827 

828 

829def _render_verification__sv_success(loc_context: LocalizationContext) -> PushNotificationContent: 

830 return _get_content( 

831 NotificationTopicAction.verification__sv_success, 

832 loc_context, 

833 action_url=urls.account_settings_link(), 

834 ) 

835 

836 

837def _render_verification__sv_fail( 

838 data: notification_data_pb2.VerificationSVFail, loc_context: LocalizationContext 

839) -> PushNotificationContent: 

840 body_key: str 

841 match data.reason: 

842 case notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER: 842 ↛ 843line 842 didn't jump to line 843 because the pattern on line 842 never matched

843 body_key = "body_wrong_birthdate_gender" 

844 case notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT: 

845 body_key = "body_not_a_passport" 

846 case notification_data_pb2.SV_FAIL_REASON_DUPLICATE: 846 ↛ 848line 846 didn't jump to line 848 because the pattern on line 846 always matched

847 body_key = "body_duplicate" 

848 case _: 

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

850 

851 return _get_content( 

852 NotificationTopicAction.verification__sv_fail, 

853 loc_context, 

854 body=_get_string(NotificationTopicAction.verification__sv_fail, body_key, loc_context), 

855 action_url=urls.account_settings_link(), 

856 )