Coverage for src/couchers/tasks.py: 97%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

165 statements  

1import logging 

2from datetime import timedelta 

3from typing import List 

4 

5from sqlalchemy.sql import func 

6 

7from couchers import email, urls 

8from couchers.config import config 

9from couchers.constants import SIGNUP_EMAIL_TOKEN_VALIDITY 

10from couchers.crypto import urlsafe_secure_token 

11from couchers.db import session_scope 

12from couchers.models import ( 

13 AccountDeletionToken, 

14 ClusterRole, 

15 ClusterSubscription, 

16 LoginToken, 

17 Node, 

18 Notification, 

19 PasswordResetToken, 

20 User, 

21) 

22from couchers.notifications.unsubscribe import generate_mute_all, generate_unsub_topic_action, generate_unsub_topic_key 

23from couchers.sql import couchers_select as select 

24from couchers.utils import now 

25 

26logger = logging.getLogger(__name__) 

27 

28 

29def send_signup_email(flow): 

30 logger.info(f"Sending signup email to {flow.email=}:") 

31 

32 # whether we've sent an email at all yet 

33 email_sent_before = flow.email_sent 

34 if flow.email_verified: 

35 # we just send a link to continue, not a verification link 

36 signup_link = urls.signup_link(token=flow.flow_token) 

37 elif flow.email_token and flow.token_is_valid: 

38 # if the verification email was sent and still is not expired, just resend the verification email 

39 signup_link = urls.signup_link(token=flow.email_token) 

40 else: 

41 # otherwise send a fresh email with new token 

42 token = urlsafe_secure_token() 

43 flow.email_verified = False 

44 flow.email_token = token 

45 flow.email_token_expiry = now() + SIGNUP_EMAIL_TOKEN_VALIDITY 

46 signup_link = urls.signup_link(token=flow.email_token) 

47 

48 flow.email_sent = True 

49 

50 logger.info(f"Link is: {signup_link}") 

51 template = "signup_verify" if not email_sent_before else "signup_continue" 

52 email.enqueue_email_from_template(flow.email, template, template_args={"flow": flow, "signup_link": signup_link}) 

53 

54 

55def send_login_email(session, user): 

56 login_token = LoginToken(token=urlsafe_secure_token(), user=user, expiry=now() + timedelta(hours=2)) 

57 session.add(login_token) 

58 

59 logger.info(f"Sending login email to {user=}:") 

60 logger.info(f"Email for {user.username=} to {user.email=}") 

61 logger.info(f"Token: {login_token=} ({login_token.created=}") 

62 login_link = urls.login_link(login_token=login_token.token) 

63 logger.info(f"Link is: {login_link}") 

64 email.enqueue_email_from_template(user.email, "login", template_args={"user": user, "login_link": login_link}) 

65 

66 return login_token 

67 

68 

69def send_api_key_email(session, user, token, expiry): 

70 logger.info(f"Sending API key email to {user=}:") 

71 email.enqueue_email_from_template( 

72 user.email, "api_key", template_args={"user": user, "token": token, "expiry": expiry} 

73 ) 

74 

75 

76def send_password_reset_email(session, user): 

77 password_reset_token = PasswordResetToken( 

78 token=urlsafe_secure_token(), user=user, expiry=now() + timedelta(hours=2) 

79 ) 

80 session.add(password_reset_token) 

81 

82 logger.info(f"Sending password reset email to {user=}:") 

83 password_reset_link = urls.password_reset_link(password_reset_token=password_reset_token.token) 

84 logger.info(f"Link is: {password_reset_link}") 

85 email.enqueue_email_from_template( 

86 user.email, "password_reset", template_args={"user": user, "password_reset_link": password_reset_link} 

87 ) 

88 

89 return password_reset_token 

90 

91 

92def send_new_host_request_email(host_request): 

93 logger.info(f"Sending host request email to {host_request.host=}:") 

94 logger.info(f"Host request sent by {host_request.surfer}") 

95 logger.info(f"Email for {host_request.host.username=} sent to {host_request.host.email=}") 

96 

97 email.enqueue_email_from_template( 

98 host_request.host.email, 

99 "host_request", 

100 template_args={ 

101 "host_request": host_request, 

102 "host_request_link": urls.host_request_link_host(), 

103 }, 

104 ) 

105 

106 

107def send_host_request_accepted_email_to_guest(host_request): 

108 logger.info(f"Sending host request accepted email to guest: {host_request.surfer=}:") 

109 logger.info(f"Email for {host_request.surfer.username=} sent to {host_request.surfer.email=}") 

110 

111 email.enqueue_email_from_template( 

112 host_request.surfer.email, 

113 "host_request_accepted_guest", 

114 template_args={ 

115 "host_request": host_request, 

116 "host_request_link": urls.host_request_link_guest(), 

117 }, 

118 ) 

119 

120 

121def send_host_request_rejected_email_to_guest(host_request): 

122 logger.info(f"Sending host request rejected email to guest: {host_request.surfer=}:") 

123 logger.info(f"Email for {host_request.surfer.username=} sent to {host_request.surfer.email=}") 

124 

125 email.enqueue_email_from_template( 

126 host_request.surfer.email, 

127 "host_request_rejected_guest", 

128 template_args={ 

129 "host_request": host_request, 

130 "host_request_link": urls.host_request_link_guest(), 

131 }, 

132 ) 

133 

134 

135def send_host_request_confirmed_email_to_host(host_request): 

136 logger.info(f"Sending host request confirmed email to host: {host_request.host=}:") 

137 logger.info(f"Email for {host_request.host.username=} sent to {host_request.host.email=}") 

138 

139 email.enqueue_email_from_template( 

140 host_request.host.email, 

141 "host_request_confirmed_host", 

142 template_args={ 

143 "host_request": host_request, 

144 "host_request_link": urls.host_request_link_host(), 

145 }, 

146 ) 

147 

148 

149def send_host_request_cancelled_email_to_host(host_request): 

150 logger.info(f"Sending host request cancelled email to host: {host_request.host=}:") 

151 logger.info(f"Email for {host_request.host.username=} sent to {host_request.host.email=}") 

152 

153 email.enqueue_email_from_template( 

154 host_request.host.email, 

155 "host_request_cancelled_host", 

156 template_args={ 

157 "host_request": host_request, 

158 "host_request_link": urls.host_request_link_host(), 

159 }, 

160 ) 

161 

162 

163def send_friend_request_email(friend_relationship): 

164 friend_requests_link = urls.friend_requests_link() 

165 

166 logger.info(f"Sending friend request email to {friend_relationship.to_user=}:") 

167 logger.info(f"Email for {friend_relationship.to_user.username=} sent to {friend_relationship.to_user.email=}") 

168 logger.info(f"Friend request sent by {friend_relationship.from_user.username=}") 

169 

170 email.enqueue_email_from_template( 

171 friend_relationship.to_user.email, 

172 "friend_request", 

173 template_args={ 

174 "friend_relationship": friend_relationship, 

175 "friend_requests_link": friend_requests_link, 

176 }, 

177 ) 

178 

179 

180def send_friend_request_accepted_email(friend_relationship): 

181 logger.info(f"Sending friend request acceptance email to {friend_relationship.from_user=}:") 

182 logger.info(f"Email for {friend_relationship.from_user.username=} sent to {friend_relationship.from_user.email=}") 

183 

184 email.enqueue_email_from_template( 

185 friend_relationship.from_user.email, 

186 "friend_request_accepted", 

187 template_args={ 

188 "friend_relationship": friend_relationship, 

189 "to_user_user_link": urls.user_link(username=friend_relationship.to_user.username), 

190 }, 

191 ) 

192 

193 

194def send_host_reference_email(reference, both_written): 

195 """ 

196 both_written == true if both the surfer and hoster wrote a reference 

197 """ 

198 assert reference.host_request_id 

199 

200 logger.info(f"Sending host reference email to {reference.to_user=} for {reference.id=}") 

201 

202 surfed = reference.host_request.surfer_user_id != reference.from_user_id 

203 

204 email.enqueue_email_from_template( 

205 reference.to_user.email, 

206 "host_reference", 

207 template_args={ 

208 "reference": reference, 

209 "leave_reference_link": urls.leave_reference_link( 

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

211 to_user_id=reference.from_user_id, 

212 host_request_id=reference.host_request.conversation_id, 

213 ), 

214 # if this reference was written by the surfer, then the recipient hosted 

215 "surfed": surfed, 

216 "both_written": both_written, 

217 }, 

218 ) 

219 

220 

221def send_friend_reference_email(reference): 

222 assert not reference.host_request_id 

223 

224 logger.info(f"Sending friend reference email to {reference.to_user=} for {reference.id=}") 

225 

226 email.enqueue_email_from_template( 

227 reference.to_user.email, 

228 "friend_reference", 

229 template_args={ 

230 "reference": reference, 

231 }, 

232 ) 

233 

234 

235def send_reference_reminder_email(user, other_user, host_request, surfed, time_left_text): 

236 logger.info(f"Sending host reference email to {user=}, they have {time_left_text} left to write a ref") 

237 

238 email.enqueue_email_from_template( 

239 user.email, 

240 "reference_reminder", 

241 template_args={ 

242 "user": user, 

243 "other_user": other_user, 

244 "host_request": host_request, 

245 "leave_reference_link": urls.leave_reference_link( 

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

247 to_user_id=other_user.id, 

248 host_request_id=host_request.conversation_id, 

249 ), 

250 "surfed": surfed, 

251 "time_left_text": time_left_text, 

252 }, 

253 ) 

254 

255 

256def send_password_changed_email(user): 

257 """ 

258 Send the user an email saying their password has been changed. 

259 """ 

260 logger.info(f"Sending password changed (notification) email to {user=}") 

261 email.enqueue_email_from_template(user.email, "password_changed", template_args={"user": user}) 

262 

263 

264def send_email_changed_notification_email(user): 

265 """ 

266 Send an email to user's original address notifying that it has been changed 

267 """ 

268 logger.info( 

269 f"Sending email changed (notification) email to {user=} (old email: {user.email=}, new email: {user.new_email=})" 

270 ) 

271 email.enqueue_email_from_template(user.email, "email_changed_notification", template_args={"user": user}) 

272 

273 

274def send_email_changed_confirmation_to_old_email(user): 

275 """ 

276 Send an email to user's original email address requesting confirmation of email change 

277 """ 

278 logger.info( 

279 f"Sending email changed (confirmation) email to {user=}'s old email address, (old email: {user.email}, new email: {user.new_email=})" 

280 ) 

281 

282 confirmation_link = urls.change_email_link(confirmation_token=user.old_email_token) 

283 email.enqueue_email_from_template( 

284 user.email, 

285 "email_changed_confirmation_old_email", 

286 template_args={"user": user, "confirmation_link": confirmation_link}, 

287 ) 

288 

289 

290def send_email_changed_confirmation_to_new_email(user): 

291 """ 

292 Send an email to user's new email address requesting confirmation of email change 

293 """ 

294 logger.info( 

295 f"Sending email changed (confirmation) email to {user=}'s new email address, (old email: {user.email}, new email: {user.new_email=})" 

296 ) 

297 

298 confirmation_link = urls.change_email_link(confirmation_token=user.new_email_token) 

299 email.enqueue_email_from_template( 

300 user.new_email, 

301 "email_changed_confirmation_new_email", 

302 template_args={"user": user, "confirmation_link": confirmation_link}, 

303 ) 

304 

305 

306def send_onboarding_email(user, email_number): 

307 email.enqueue_email_from_template( 

308 user.email, 

309 f"onboarding{email_number}", 

310 template_args={ 

311 "user": user, 

312 "app_link": urls.app_link(), 

313 "profile_link": urls.profile_link(), 

314 "edit_profile_link": urls.edit_profile_link(), 

315 }, 

316 ) 

317 

318 

319def send_donation_email(user, amount, receipt_url): 

320 email.enqueue_email_from_template( 

321 user.email, 

322 "donation_received", 

323 template_args={"user": user, "amount": amount, "receipt_url": receipt_url}, 

324 ) 

325 

326 

327def send_content_report_email(content_report): 

328 target_email = config["REPORTS_EMAIL_RECIPIENT"] 

329 

330 logger.info(f"Sending content report email to {target_email=}") 

331 email.enqueue_email_from_template( 

332 target_email, 

333 "content_report", 

334 template_args={ 

335 "report": content_report, 

336 "author_user_user_link": urls.user_link(username=content_report.author_user.username), 

337 "reporting_user_user_link": urls.user_link(username=content_report.reporting_user.username), 

338 }, 

339 ) 

340 

341 

342def maybe_send_reference_report_email(reference): 

343 target_email = config["REPORTS_EMAIL_RECIPIENT"] 

344 

345 if reference.should_report: 

346 logger.info(f"Sending reference report email to {target_email=}") 

347 email.enqueue_email_from_template( 

348 target_email, 

349 "reference_report", 

350 template_args={ 

351 "reference": reference, 

352 "from_user_user_link": urls.user_link(username=reference.from_user.username), 

353 "to_user_user_link": urls.user_link(username=reference.to_user.username), 

354 }, 

355 ) 

356 

357 

358def maybe_send_contributor_form_email(form): 

359 target_email = config["CONTRIBUTOR_FORM_EMAIL_RECIPIENT"] 

360 

361 if form.should_notify: 

362 email.enqueue_email_from_template( 

363 target_email, 

364 "contributor_form", 

365 template_args={"form": form, "user_link": urls.user_link(username=form.user.username)}, 

366 ) 

367 

368 

369def send_digest_email(notifications: List[Notification]): 

370 logger.info(f"Sending digest email to {notification.user=}:") 

371 email.enqueue_email_from_template( 

372 notification.user.email, 

373 "digest", 

374 template_args={"notifications": notifications}, 

375 ) 

376 

377 

378def send_notification_email(notification: Notification): 

379 friend_requests_link = urls.friend_requests_link() 

380 logger.info(f"Sending notification email to {notification.user=}:") 

381 

382 email.enqueue_email_from_template( 

383 notification.user.email, 

384 "notification", 

385 template_args={ 

386 "notification": notification, 

387 "unsub_all": generate_mute_all(notification.user_id), 

388 "unsub_topic_key": generate_unsub_topic_key(notification), 

389 "unsub_topic_action": generate_unsub_topic_action(notification), 

390 }, 

391 ) 

392 

393 

394def enforce_community_memberships(): 

395 """ 

396 Go through all communities and make sure every user in the polygon is also a member 

397 """ 

398 with session_scope() as session: 

399 for node in session.execute(select(Node)).scalars().all(): 

400 existing_users = select(ClusterSubscription.user_id).where( 

401 ClusterSubscription.cluster == node.official_cluster 

402 ) 

403 users_needing_adding = ( 

404 session.execute( 

405 select(User) 

406 .where(User.is_visible) 

407 .where(func.ST_Contains(node.geom, User.geom)) 

408 .where(~User.id.in_(existing_users)) 

409 ) 

410 .scalars() 

411 .all() 

412 ) 

413 for user in users_needing_adding: 

414 node.official_cluster.cluster_subscriptions.append( 

415 ClusterSubscription( 

416 user=user, 

417 role=ClusterRole.member, 

418 ) 

419 ) 

420 session.commit() 

421 

422 

423def send_account_deletion_confirmation_email(user): 

424 logger.info(f"Sending account deletion confirmation email to {user=}.") 

425 logger.info(f"Email for {user.username=} sent to {user.email}.") 

426 token = AccountDeletionToken(token=urlsafe_secure_token(), user=user, expiry=now() + timedelta(hours=2)) 

427 deletion_link = urls.delete_account_link(account_deletion_token=token.token) 

428 email.enqueue_email_from_template( 

429 user.email, 

430 "account_deletion_confirmation", 

431 template_args={"user": user, "deletion_link": deletion_link}, 

432 ) 

433 

434 return token 

435 

436 

437def send_account_deletion_successful_email(user, undelete_days): 

438 logger.info(f"Sending account deletion successful email to {user=}.") 

439 logger.info(f"Email for {user.username=} sent to {user.email}.") 

440 undelete_link = urls.recover_account_link(account_undelete_token=user.undelete_token) 

441 email.enqueue_email_from_template( 

442 user.email, 

443 "account_deletion_successful", 

444 template_args={"user": user, "undelete_link": undelete_link, "days": undelete_days}, 

445 ) 

446 

447 

448def send_account_recovered_email(user): 

449 logger.info(f"Sending account recovered successful email to {user=}.") 

450 logger.info(f"Email for {user.username=} sent to {user.email}.") 

451 email.enqueue_email_from_template( 

452 user.email, 

453 "account_recovered_successful", 

454 template_args={"user": user, "app_link": urls.app_link()}, 

455 ) 

456 

457 

458def send_account_deletion_report_email(reason): 

459 target_email = config["REPORTS_EMAIL_RECIPIENT"] 

460 

461 logger.info(f"Sending account deletion report email to {target_email=}") 

462 email.enqueue_email_from_template( 

463 target_email, 

464 "account_deletion_report", 

465 template_args={ 

466 "reason": reason, 

467 }, 

468 ) 

469 

470 

471def enforce_community_memberships_for_user(session, user): 

472 """ 

473 Adds a given user to all the communities they belong in based on their location. 

474 """ 

475 nodes = session.execute(select(Node).where(func.ST_Contains(Node.geom, user.geom))).scalars().all() 

476 for node in nodes: 

477 node.official_cluster.cluster_subscriptions.append( 

478 ClusterSubscription( 

479 user=user, 

480 role=ClusterRole.member, 

481 ) 

482 ) 

483 session.commit()