|
| 1 | +from sqlalchemy import and_ |
| 2 | +from main import db |
| 3 | +from datetime import datetime, timedelta |
| 4 | +from flask import current_app as app |
| 5 | + |
| 6 | +from models import scheduled_task |
| 7 | +from models.cfp import Proposal |
| 8 | +from models.user import User |
| 9 | +from models.volunteer.shift import Shift, ShiftEntry |
| 10 | +from models.web_push import PushNotificationJob, enqueue_if_not_exists |
| 11 | +from models.notifications import UserNotificationPreference |
| 12 | +from pywebpush import webpush, WebPushException |
| 13 | + |
| 14 | + |
| 15 | +def deliver_notification(job: PushNotificationJob): |
| 16 | + """Deliver a push notification from a PushNotificationJob. |
| 17 | +
|
| 18 | + The passed job will be mutated to reflect delivery state. A job which isn't |
| 19 | + queued will be skipped over. |
| 20 | + """ |
| 21 | + if job.state != "queued": |
| 22 | + return |
| 23 | + |
| 24 | + try: |
| 25 | + webpush( |
| 26 | + subscription_info=job.target.subscription_info, |
| 27 | + data=job.title, |
| 28 | + vapid_private_key=app.config["WEBPUSH_PRIVATE_KEY"], |
| 29 | + vapid_claims={ |
| 30 | + "sub": "mailto:[email protected]", |
| 31 | + }, |
| 32 | + ) |
| 33 | + |
| 34 | + job.state = "delivered" |
| 35 | + except WebPushException as err: |
| 36 | + job.state = "failed" |
| 37 | + job.error = err.message |
| 38 | + |
| 39 | + |
| 40 | +@scheduled_task(minutes=1) |
| 41 | +def send_queued_notifications(): |
| 42 | + jobs = PushNotificationJob.query.where( |
| 43 | + PushNotificationJob.state == "queued" |
| 44 | + and (PushNotificationJob.not_before is None or PushNotificationJob.not_before <= datetime.now()) |
| 45 | + ).all() |
| 46 | + |
| 47 | + for job in jobs: |
| 48 | + deliver_notification(job) |
| 49 | + db.session.add(job) |
| 50 | + |
| 51 | + db.session.commit() |
| 52 | + |
| 53 | + |
| 54 | +@scheduled_task(minutes=15) |
| 55 | +def queue_content_notifications(time=None) -> None: |
| 56 | + if time is None: |
| 57 | + time = datetime.now() |
| 58 | + |
| 59 | + users = User.query.join( |
| 60 | + UserNotificationPreference, |
| 61 | + User.notification_preferences.and_(UserNotificationPreference.favourited_content), |
| 62 | + ) |
| 63 | + |
| 64 | + upcoming_content = Proposal.query.filter( |
| 65 | + and_(Proposal.scheduled_time >= time, Proposal.scheduled_time <= time + timedelta(minutes=16)) |
| 66 | + ).all() |
| 67 | + |
| 68 | + for user in users: |
| 69 | + user_favourites = [f.id for f in user.favourites] |
| 70 | + favourites = [p for p in upcoming_content if p.id in user_favourites] |
| 71 | + for proposal in favourites: |
| 72 | + for target in user.web_push_targets: |
| 73 | + enqueue_if_not_exists( |
| 74 | + target=target, |
| 75 | + related_to=f"favourite,user:{user.id},proposal:{proposal.id},target:{target.id}", |
| 76 | + title=f"{proposal.title} is happening soon at {proposal.scheduled_venue.name}", |
| 77 | + not_before=proposal.scheduled_time - timedelta(minutes=15), |
| 78 | + ) |
| 79 | + |
| 80 | + db.session.commit() |
| 81 | + |
| 82 | + |
| 83 | +@scheduled_task(minutes=15) |
| 84 | +def queue_shift_notifications(time=None) -> None: |
| 85 | + if time is None: |
| 86 | + time = datetime.now() |
| 87 | + |
| 88 | + upcoming_shifts: list[Shift] = Shift.query.filter( |
| 89 | + and_(Shift.start >= time, Shift.start <= time + timedelta(minutes=16)) |
| 90 | + ).all() |
| 91 | + |
| 92 | + for shift in upcoming_shifts: |
| 93 | + for user in shift.volunteers: |
| 94 | + if user.notification_preferences.volunteer_shifts: |
| 95 | + for target in user.web_push_targets: |
| 96 | + enqueue_if_not_exists( |
| 97 | + target=target, |
| 98 | + related_to=f"shift_reminder,user:{user.id},shift:{shift.id},target:{target.id}", |
| 99 | + title=f"Your {shift.role.name} shift is about to start, please go to {shift.venue.name}.", |
| 100 | + not_before=shift.start - timedelta(minutes=15), |
| 101 | + ) |
| 102 | + |
| 103 | + db.session.commit() |
0 commit comments