Coverage for src / couchers / notifications / render_push.py: 83%

273 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-07 16:21 +0000

1""" 

2Renders a Notification model into a localized push notification. 

3""" 

4 

5import logging 

6from typing import Any, assert_never 

7 

8from couchers import urls 

9from couchers.models import Notification, NotificationTopicAction, User 

10from couchers.notifications.push import PushNotificationContent 

11from couchers.proto import events_pb2, notification_data_pb2 

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

13 

14logger = logging.getLogger(__name__) 

15 

16# Best practices for push notification strings (Android/iOS lowest common denominator): 

17# Title: 

18# - Describe the event, e.g. "Payment Successful" 

19# - <= 30 chars (Android), most important info in first 20 chars 

20# - Title-style capitalization, no ending punctuation 

21# Body: 

22# - <= 80 chars (Android), first 40 visible when collapsed 

23# - Sentence-style capitalization with punctuation 

24 

25 

26def render_push_notification(user: User, notification: Notification) -> PushNotificationContent: 

27 # Any-typed so it implicitly converts when calling the methods below 

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

29 match notification.topic_action: 

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

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

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

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

34 case NotificationTopicAction.account_deletion__start: 

35 return _account_deletion__start(data) 

36 case NotificationTopicAction.account_deletion__complete: 

37 return _account_deletion__complete(data) 

38 case NotificationTopicAction.account_deletion__recovered: 

39 return _account_deletion__recovered() 

40 case NotificationTopicAction.activeness__probe: 

41 return _activeness__probe(data) 

42 case NotificationTopicAction.api_key__create: 

43 return _api_key__create(data) 

44 case NotificationTopicAction.badge__add: 

45 return _badge__add(data) 

46 case NotificationTopicAction.badge__remove: 

47 return _badge__remove(data) 

48 case NotificationTopicAction.birthdate__change: 

49 return _birthdate__change(data, user) 

50 case NotificationTopicAction.chat__message: 

51 return _chat__message(data) 

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

53 return _chat__missed_messages() 

54 case NotificationTopicAction.donation__received: 

55 return _donation__received(data) 

56 case NotificationTopicAction.discussion__create: 

57 return _discussion__create(data) 

58 case NotificationTopicAction.discussion__comment: 

59 return _discussion__comment(data) 

60 case NotificationTopicAction.email_address__change: 

61 return _email_address__change(data) 

62 case NotificationTopicAction.email_address__verify: 

63 return _email_address__verify() 

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

65 return _event__create_any(data, user) 

66 case NotificationTopicAction.event__create_approved: 

67 return _event__create_approved(data, user) 

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

69 return _event__update(data, user) 

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

71 return _event__invite_organizer(data, user) 

72 case NotificationTopicAction.event__comment: 

73 return _event__comment(data, user) 

74 case NotificationTopicAction.event__reminder: 

75 return _event__reminder(data, user) 

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

77 return _event__cancel(data, user) 

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

79 return _event__delete(data, user) 

80 case NotificationTopicAction.friend_request__create: 

81 return _friend_request__create(data) 

82 case NotificationTopicAction.friend_request__accept: 

83 return _friend_request__accept(data) 

84 case NotificationTopicAction.gender__change: 

85 return _gender__change(data) 

86 case NotificationTopicAction.general__new_blog_post: 

87 return _general__new_blog_post(data) 

88 case NotificationTopicAction.host_request__create: 

89 return _host_request__create(data, user) 

90 case NotificationTopicAction.host_request__message: 

91 return _host_request__message(data, user) 

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

93 return _host_request__missed_messages(data) 

94 case NotificationTopicAction.host_request__reminder: 

95 return _host_request__reminder(data) 

96 case NotificationTopicAction.host_request__accept: 

97 return _host_request__accept(data) 

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

99 return _host_request__reject(data) 

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

101 return _host_request__cancel(data) 

102 case NotificationTopicAction.host_request__confirm: 

103 return _host_request__confirm(data) 

104 case NotificationTopicAction.modnote__create: 

105 return _modnote__create() 

106 case NotificationTopicAction.onboarding__reminder: 

107 return _onboarding__reminder(notification.key, user) 

108 case NotificationTopicAction.password__change: 

109 return _password__change() 

110 case NotificationTopicAction.password_reset__start: 

111 return _password_reset__start(data) 

112 case NotificationTopicAction.password_reset__complete: 

113 return _password_reset__complete() 

114 case NotificationTopicAction.phone_number__change: 

115 return _phone_number__change(data) 

116 case NotificationTopicAction.phone_number__verify: 

117 return _phone_number__verify(data) 

118 case NotificationTopicAction.postal_verification__postcard_sent: 

119 return _postal_verification__postcard_sent(data) 

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

121 return _postal_verification__success() 

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

123 return _postal_verification__failed(data) 

124 case NotificationTopicAction.reference__receive_friend: 

125 return _reference__receive_friend(data) 

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

127 return _reference__receive_hosted(data) 

128 case NotificationTopicAction.reference__receive_surfed: 

129 return _reference__receive_surfed(data) 

130 case NotificationTopicAction.reference__reminder_hosted: 

131 return _reference__reminder_hosted(data) 

132 case NotificationTopicAction.reference__reminder_surfed: 

133 return _reference__reminder_surfed(data) 

134 case NotificationTopicAction.thread__reply: 

135 return _thread__reply(data) 

136 case NotificationTopicAction.verification__sv_success: 

137 return _verification__sv_success() 

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

139 return _verification__sv_fail(data) 

140 case _: 

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

142 assert_never(notification.topic_action) 

143 

144 

145def _account_deletion__start(data: notification_data_pb2.AccountDeletionStart) -> PushNotificationContent: 

146 return PushNotificationContent( 

147 title="Account deletion initiated", 

148 body="Someone initiated the deletion of your Couchers.org account. To delete your account, please follow the link in the email we sent you.", 

149 ) 

150 

151 

152def _account_deletion__complete(data: notification_data_pb2.AccountDeletionComplete) -> PushNotificationContent: 

153 return PushNotificationContent( 

154 title="Your Couchers.org account has been deleted", 

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

156 ) 

157 

158 

159def _account_deletion__recovered() -> PushNotificationContent: 

160 return PushNotificationContent( 

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

162 body="We have recovered your Couchers.org account as per your request! Welcome back!", 

163 ) 

164 

165 

166def _activeness__probe(data: notification_data_pb2.ActivenessProbe) -> PushNotificationContent: 

167 return PushNotificationContent( 

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

169 body="Please log in to confirm your hosting status.", 

170 ) 

171 

172 

173def _api_key__create(data: notification_data_pb2.ApiKeyCreate) -> PushNotificationContent: 

174 return PushNotificationContent( 

175 title="An API key was created for your account", 

176 body="Details were sent to you via email.", 

177 action_url=urls.app_link(), 

178 ) 

179 

180 

181def _badge__add(data: notification_data_pb2.BadgeAdd) -> PushNotificationContent: 

182 return PushNotificationContent( 

183 title=f"The {data.badge_name} badge was added to your profile", 

184 body="Check out your profile to see the new badge!", 

185 action_url=urls.profile_link(), 

186 ) 

187 

188 

189def _badge__remove(data: notification_data_pb2.BadgeRemove) -> PushNotificationContent: 

190 return PushNotificationContent( 

191 title=f"The {data.badge_name} badge was removed from your profile", 

192 body="You can see all your badges on your profile.", 

193 action_url=urls.profile_link(), 

194 ) 

195 

196 

197def _birthdate__change(data: notification_data_pb2.BirthdateChange, user: User) -> PushNotificationContent: 

198 return PushNotificationContent( 

199 title="Your date of birth was changed", 

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

201 action_url=urls.account_settings_link(), 

202 ) 

203 

204 

205def _chat__message(data: notification_data_pb2.ChatMessage) -> PushNotificationContent: 

206 return PushNotificationContent( 

207 title=data.message, 

208 body=data.text, 

209 icon_url=v2avatar(data.author), 

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

211 ) 

212 

213 

214def _chat__missed_messages() -> PushNotificationContent: 

215 return PushNotificationContent( 

216 title="You have unseen messages on Couchers.org", 

217 body="Please check out any messages you missed.", 

218 action_url=urls.messages_link(), 

219 ) 

220 

221 

222def _donation__received(data: notification_data_pb2.DonationReceived) -> PushNotificationContent: 

223 return PushNotificationContent( 

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

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

226 action_url=data.receipt_url, 

227 ) 

228 

229 

230def _discussion__create(data: notification_data_pb2.DiscussionCreate) -> PushNotificationContent: 

231 return PushNotificationContent( 

232 title=data.discussion.title, 

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

234 icon_url=v2avatar(data.author), 

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

236 ) 

237 

238 

239def _discussion__comment(data: notification_data_pb2.DiscussionComment) -> PushNotificationContent: 

240 return PushNotificationContent( 

241 title=data.discussion.title, 

242 body=f"{data.author.name} commented:\n\n{data.reply.content}", 

243 icon_url=v2avatar(data.author), 

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

245 ) 

246 

247 

248def _email_address__change(data: notification_data_pb2.EmailAddressChange) -> PushNotificationContent: 

249 return PushNotificationContent( 

250 title="An email change was initiated on your account", 

251 body=f"An email change to the email {data.new_email} was initiated on your account.", 

252 action_url=urls.account_settings_link(), 

253 ) 

254 

255 

256def _email_address__verify() -> PushNotificationContent: 

257 return PushNotificationContent( 

258 title="Email change completed", 

259 body="Your new email address has been verified.", 

260 action_url=urls.account_settings_link(), 

261 ) 

262 

263 

264def _get_event_time_display(event: events_pb2.Event, user: User) -> str: 

265 return f"{v2timestamp(event.start_time, user)} - {v2timestamp(event.end_time, user)}" 

266 

267 

268def _event__create_any(data: notification_data_pb2.EventCreate, user: User) -> PushNotificationContent: 

269 time_display = _get_event_time_display(data.event, user) 

270 return PushNotificationContent( 

271 title=f'{data.inviting_user.name} created an event called "{data.event.title}"', 

272 body=f"{time_display}\nCreated by {data.inviting_user.name}\n\n{data.event.content}", 

273 icon_url=v2avatar(data.inviting_user), 

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

275 ) 

276 

277 

278def _event__create_approved(data: notification_data_pb2.EventCreate, user: User) -> PushNotificationContent: 

279 time_display = _get_event_time_display(data.event, user) 

280 return PushNotificationContent( 

281 title=f'{data.inviting_user.name} invited you to "{data.event.title}"', 

282 body=f"{time_display}\nInvited by {data.inviting_user.name}\n\n{data.event.content}", 

283 icon_url=v2avatar(data.inviting_user), 

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

285 ) 

286 

287 

288def _event__update(data: notification_data_pb2.EventUpdate, user: User) -> PushNotificationContent: 

289 time_display = _get_event_time_display(data.event, user) 

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

291 return PushNotificationContent( 

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

293 body=f"{time_display}\n{data.updating_user.name} updated: {updated_text}\n\n{data.event.content}", 

294 icon_url=v2avatar(data.updating_user), 

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

296 ) 

297 

298 

299def _event__invite_organizer(data: notification_data_pb2.EventInviteOrganizer, user: User) -> PushNotificationContent: 

300 time_display = _get_event_time_display(data.event, user) 

301 return PushNotificationContent( 

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

303 body=f"{time_display}\nInvited to co-organize by {data.inviting_user.name}\n\n{data.event.content}", 

304 icon_url=v2avatar(data.inviting_user), 

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

306 ) 

307 

308 

309def _event__comment(data: notification_data_pb2.EventComment, user: User) -> PushNotificationContent: 

310 time_display = _get_event_time_display(data.event, user) 

311 return PushNotificationContent( 

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

313 body=f"{time_display}\n{data.author.name} commented:\n\n{data.reply.content}", 

314 icon_url=v2avatar(data.author), 

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

316 ) 

317 

318 

319def _event__reminder(data: notification_data_pb2.EventReminder, user: User) -> PushNotificationContent: 

320 time_display = _get_event_time_display(data.event, user) 

321 return PushNotificationContent( 

322 title=f'"{data.event.title}" starts soon', 

323 body=f"Don't forget your upcoming event on Couchers.org\n{time_display}\n{data.event.content}", 

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

325 ) 

326 

327 

328def _event__cancel(data: notification_data_pb2.EventCancel, user: User) -> PushNotificationContent: 

329 time_display = _get_event_time_display(data.event, user) 

330 return PushNotificationContent( 

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

332 body=f"{time_display}\nThe event has been cancelled by {data.cancelling_user.name}.\n\n{data.event.content}", 

333 icon_url=v2avatar(data.cancelling_user), 

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

335 ) 

336 

337 

338def _event__delete(data: notification_data_pb2.EventDelete, user: User) -> PushNotificationContent: 

339 time_display = _get_event_time_display(data.event, user) 

340 return PushNotificationContent( 

341 title=f'A moderator deleted "{data.event.title}"', 

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

343 ) 

344 

345 

346def _friend_request__create(data: notification_data_pb2.FriendRequestCreate) -> PushNotificationContent: 

347 return PushNotificationContent( 

348 title=f"{data.other_user.name} wants to be your friend", 

349 body=f"You've received a friend request from {data.other_user.name}", 

350 icon_url=v2avatar(data.other_user), 

351 action_url=urls.friend_requests_link(), 

352 ) 

353 

354 

355def _friend_request__accept(data: notification_data_pb2.FriendRequestAccept) -> PushNotificationContent: 

356 return PushNotificationContent( 

357 title=f"{data.other_user.name} accepted your friend request!", 

358 body=f"{v2esc(data.other_user.name)} has accepted your friend request", 

359 icon_url=v2avatar(data.other_user), 

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

361 ) 

362 

363 

364def _gender__change(data: notification_data_pb2.GenderChange) -> PushNotificationContent: 

365 return PushNotificationContent( 

366 title="Your gender was changed", 

367 body=f"Your gender on Couchers.org was changed to {data.gender} by an admin.", 

368 action_url=urls.account_settings_link(), 

369 ) 

370 

371 

372def _general__new_blog_post(data: notification_data_pb2.GeneralNewBlogPost) -> PushNotificationContent: 

373 return PushNotificationContent( 

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

375 body=data.blurb, 

376 action_url=data.url, 

377 ) 

378 

379 

380def _host_request__create(data: notification_data_pb2.HostRequestCreate, user: User) -> PushNotificationContent: 

381 return PushNotificationContent( 

382 title=f"{data.surfer.name} sent you a host request", 

383 body=f"Dates: {v2date(data.host_request.from_date, user)} to {v2date(data.host_request.to_date, user)}.\n\n{data.text}", 

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

385 icon_url=v2avatar(data.surfer), 

386 ) 

387 

388 

389def _host_request__message(data: notification_data_pb2.HostRequestMessage, user: User) -> PushNotificationContent: 

390 if data.am_host: 390 ↛ 393line 390 didn't jump to line 393 because the condition on line 390 was always true

391 title = f"{data.user.name} sent you a message in their host request" 

392 else: 

393 title = f"{data.user.name} sent you a message in your host request" 

394 return PushNotificationContent( 

395 title=title, 

396 body=f"Dates: {v2date(data.host_request.from_date, user)} to {v2date(data.host_request.to_date, user)}.\n\n{data.text}", 

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

398 icon_url=v2avatar(data.user), 

399 ) 

400 

401 

402def _host_request__missed_messages(data: notification_data_pb2.HostRequestMissedMessages) -> PushNotificationContent: 

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

404 return PushNotificationContent( 

405 title=f"{data.user.name} sent you message(s) in {their_your} host request", 

406 body="Check the app for more info.", 

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

408 icon_url=v2avatar(data.user), 

409 ) 

410 

411 

412def _host_request__reminder(data: notification_data_pb2.HostRequestReminder) -> PushNotificationContent: 

413 return PushNotificationContent( 

414 title=f"You have a pending host request from {data.surfer.name}!", 

415 body="Please respond to the request!", 

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

417 icon_url=v2avatar(data.surfer), 

418 ) 

419 

420 

421def _host_request__accept(data: notification_data_pb2.HostRequestAccept) -> PushNotificationContent: 

422 return PushNotificationContent( 

423 title=f"{data.host.name} accepted your host request", 

424 body="Check the app for more info.", 

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

426 icon_url=v2avatar(data.host), 

427 ) 

428 

429 

430def _host_request__reject(data: notification_data_pb2.HostRequestReject) -> PushNotificationContent: 

431 return PushNotificationContent( 

432 title=f"{data.host.name} rejected your host request", 

433 body="Check the app for more info.", 

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

435 icon_url=v2avatar(data.host), 

436 ) 

437 

438 

439def _host_request__cancel(data: notification_data_pb2.HostRequestCancel) -> PushNotificationContent: 

440 return PushNotificationContent( 

441 title=f"{data.surfer.name} cancelled their host request", 

442 body="Check the app for more info.", 

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

444 icon_url=v2avatar(data.surfer), 

445 ) 

446 

447 

448def _host_request__confirm(data: notification_data_pb2.HostRequestConfirm) -> PushNotificationContent: 

449 return PushNotificationContent( 

450 title=f"{data.surfer.name} confirmed their host request", 

451 body="Check the app for more info.", 

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

453 icon_url=v2avatar(data.surfer), 

454 ) 

455 

456 

457def _modnote__create() -> PushNotificationContent: 

458 return PushNotificationContent( 

459 title="You received a mod note", 

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

461 ) 

462 

463 

464def _onboarding__reminder(key: str, user: User) -> PushNotificationContent: 

465 if key == "1": 

466 return PushNotificationContent( 

467 title="Welcome to Couchers.org and the future of couch surfing", 

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

469 action_url=urls.edit_profile_link(), 

470 ) 

471 elif key == "2": 

472 return PushNotificationContent( 

473 title="Please complete your profile on Couchers.org!", 

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

475 action_url=urls.edit_profile_link(), 

476 ) 

477 else: 

478 raise NotImplementedError(f"Unknown onboarding reminder key: {key}") 

479 

480 

481def _password__change() -> PushNotificationContent: 

482 return PushNotificationContent( 

483 title="Your password was changed", 

484 body="Your login password for Couchers.org was changed.", 

485 action_url=urls.account_settings_link(), 

486 ) 

487 

488 

489def _password_reset__start(data: notification_data_pb2.PasswordResetStart) -> PushNotificationContent: 

490 return PushNotificationContent( 

491 title="A password reset was initiated on your account", 

492 body="Someone initiated a password change on your account.", 

493 action_url=urls.account_settings_link(), 

494 ) 

495 

496 

497def _password_reset__complete() -> PushNotificationContent: 

498 return PushNotificationContent( 

499 title="Your password was successfully reset", 

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

501 action_url=urls.account_settings_link(), 

502 ) 

503 

504 

505def _phone_number__change(data: notification_data_pb2.PhoneNumberChange) -> PushNotificationContent: 

506 return PushNotificationContent( 

507 title="Phone verification started", 

508 body=f"You started phone number verification with the number {v2phone(data.phone)}.", 

509 action_url=urls.feature_preview_link(), 

510 ) 

511 

512 

513def _phone_number__verify(data: notification_data_pb2.PhoneNumberVerify) -> PushNotificationContent: 

514 return PushNotificationContent( 

515 title="Phone successfully verified", 

516 body=f"Your phone was successfully verified as {v2phone(data.phone)} on Couchers.org.", 

517 action_url=urls.feature_preview_link(), 

518 ) 

519 

520 

521def _postal_verification__postcard_sent( 

522 data: notification_data_pb2.PostalVerificationPostcardSent, 

523) -> PushNotificationContent: 

524 return PushNotificationContent( 

525 title="Your verification postcard is on its way", 

526 body=f"Postcard sent to {data.city}, {data.country}. Expect it within 1-3 weeks.", 

527 action_url=urls.account_settings_link(), 

528 ) 

529 

530 

531def _postal_verification__success() -> PushNotificationContent: 

532 return PushNotificationContent( 

533 title="Postal Verification succeeded", 

534 body="You have been verified with Postal Verification! Your address has been confirmed.", 

535 action_url=urls.account_settings_link(), 

536 ) 

537 

538 

539def _postal_verification__failed(data: notification_data_pb2.PostalVerificationFailed) -> PushNotificationContent: 

540 if data.reason == notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_CODE_EXPIRED: 

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

542 elif data.reason == notification_data_pb2.POSTAL_VERIFICATION_FAIL_REASON_TOO_MANY_ATTEMPTS: 

543 reason_message = "Too many incorrect code attempts. You can start a new verification attempt." 

544 else: 

545 reason_message = "Your postal verification attempt has failed. You can start a new verification attempt." 

546 return PushNotificationContent( 

547 title="Postal Verification failed", 

548 body=reason_message, 

549 action_url=urls.account_settings_link(), 

550 ) 

551 

552 

553def _reference__receive_friend(data: notification_data_pb2.ReferenceReceiveFriend) -> PushNotificationContent: 

554 return PushNotificationContent( 

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

556 body=data.text, 

557 icon_url=v2avatar(data.from_user), 

558 action_url=urls.profile_references_link(), 

559 ) 

560 

561 

562def _reference__receive( 

563 data: notification_data_pb2.ReferenceReceiveHostRequest, reference_type: str 

564) -> PushNotificationContent: 

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

566 body = v2esc(data.text) 

567 action_url = urls.profile_references_link() 

568 else: 

569 body = ( 

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

571 ) 

572 action_url = urls.leave_reference_link( 

573 reference_type=reference_type, 

574 to_user_id=data.from_user.user_id, 

575 host_request_id=str(data.host_request_id), 

576 ) 

577 return PushNotificationContent( 

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

579 body=body, 

580 icon_url=v2avatar(data.from_user), 

581 action_url=action_url, 

582 ) 

583 

584 

585def _reference__receive_hosted(data: notification_data_pb2.ReferenceReceiveHostRequest) -> PushNotificationContent: 

586 # I surfed with them if I received a "hosted" request 

587 return _reference__receive(data, reference_type="surfed") 

588 

589 

590def _reference__receive_surfed(data: notification_data_pb2.ReferenceReceiveHostRequest) -> PushNotificationContent: 

591 return _reference__receive(data, reference_type="hosted") 

592 

593 

594def _reference__reminder(data: notification_data_pb2.ReferenceReminder, reference_type: str) -> PushNotificationContent: 

595 leave_reference_link = urls.leave_reference_link( 

596 reference_type=reference_type, 

597 to_user_id=data.other_user.user_id, 

598 host_request_id=str(data.host_request_id), 

599 ) 

600 return PushNotificationContent( 

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

602 body="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.", 

603 icon_url=v2avatar(data.other_user), 

604 action_url=leave_reference_link, 

605 ) 

606 

607 

608def _reference__reminder_surfed(data: notification_data_pb2.ReferenceReminder) -> PushNotificationContent: 

609 # I surfed with them if I get a surfed reminder 

610 return _reference__reminder(data, reference_type="surfed") 

611 

612 

613def _reference__reminder_hosted(data: notification_data_pb2.ReferenceReminder) -> PushNotificationContent: 

614 return _reference__reminder(data, reference_type="hosted") 

615 

616 

617def _thread__reply(data: notification_data_pb2.ThreadReply) -> PushNotificationContent: 

618 parent = data.WhichOneof("reply_parent") 

619 if parent == "event": 

620 title = data.event.title 

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

622 elif parent == "discussion": 622 ↛ 626line 622 didn't jump to line 626 because the condition on line 622 was always true

623 title = data.discussion.title 

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

625 else: 

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

627 

628 return PushNotificationContent( 

629 title=title, 

630 body=f"{data.author.name} replied:\n\n{data.reply.content}", 

631 icon_url=v2avatar(data.author), 

632 action_url=view_link, 

633 ) 

634 

635 

636def _verification__sv_success() -> PushNotificationContent: 

637 return PushNotificationContent( 

638 title="Strong Verification succeeded", 

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

640 action_url=urls.account_settings_link(), 

641 ) 

642 

643 

644def _verification__sv_fail(data: notification_data_pb2.VerificationSVFail) -> PushNotificationContent: 

645 if data.reason == notification_data_pb2.SV_FAIL_REASON_WRONG_BIRTHDATE_OR_GENDER: 645 ↛ 646line 645 didn't jump to line 646 because the condition on line 645 was never true

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

647 elif data.reason == notification_data_pb2.SV_FAIL_REASON_NOT_A_PASSPORT: 

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

649 elif data.reason == notification_data_pb2.SV_FAIL_REASON_DUPLICATE: 649 ↛ 652line 649 didn't jump to line 652 because the condition on line 649 was always true

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

651 else: 

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

653 return PushNotificationContent( 

654 title="Strong Verification failed", 

655 body=reason_message, 

656 action_url=urls.account_settings_link(), 

657 )