Esempio n. 1
0
def notify_groups():
    # Send out the group notifications.
    all_groups = EligibilityGroup.query.all()
    now = datetime.now()
    group_backoff_time = timedelta(
        seconds=current_app.config["GROUP_NOTIFICATION_BACKOFF"])
    group_subs_email = GroupSubscription.query.filter(
        and_(
            GroupSubscription.status == Status.CONFIRMED,
            GroupSubscription.push_sub.is_(None),
            or_(
                GroupSubscription.last_notification_at.is_(None),
                GroupSubscription.last_notification_at <
                now - group_backoff_time)))
    for subscription in group_subs_email:
        try:
            new_subscription_groups = set(
                map(attrgetter("item_description"),
                    set(all_groups) - set(subscription.known_groups)))
            if new_subscription_groups:
                logging.info(
                    f"Sending group notification to [{subscription.id}] {remove_pii(subscription.email)}."
                )
                with transaction():
                    email_notification_group.delay(
                        subscription.email,
                        hexlify(subscription.secret).decode(),
                        list(new_subscription_groups))
                    subscription.last_notification_at = now
                    subscription.known_groups = all_groups
        except (ObjectDeletedError, StaleDataError) as e:
            logging.warn(f"Got some races: {e}")

    group_subs_push = GroupSubscription.query.filter(
        and_(
            GroupSubscription.status == Status.CONFIRMED,
            GroupSubscription.email.is_(None),
            or_(
                GroupSubscription.last_notification_at.is_(None),
                GroupSubscription.last_notification_at <
                now - group_backoff_time)))
    for subscription in group_subs_push:
        try:
            new_subscription_groups = set(
                map(attrgetter("item_description"),
                    set(all_groups) - set(subscription.known_groups)))
            if new_subscription_groups:
                logging.info(
                    f"Sending group notification to [{subscription.id}].")
                with transaction():
                    push_notification_group.delay(
                        json.loads(subscription.push_sub),
                        hexlify(subscription.secret).decode(),
                        list(new_subscription_groups))
                    subscription.last_notification_at = now
                    subscription.known_groups = all_groups
        except (ObjectDeletedError, StaleDataError) as e:
            logging.warn(f"Got some races: {e}")
Esempio n. 2
0
def clear_db_unconfirmed():
    now = datetime.now()
    notification_clear_time = timedelta(
        seconds=current_app.config["NOTIFICATION_UNCONFIRMED_CLEAR"])
    to_clear_group = GroupSubscription.query.filter(
        and_(GroupSubscription.status == Status.UNCONFIRMED,
             GroupSubscription.created_at <
             now - notification_clear_time)).all()
    to_clear_spot = SpotSubscription.query.filter(
        and_(SpotSubscription.status == Status.UNCONFIRMED,
             SpotSubscription.created_at <
             now - notification_clear_time)).all()
    logging.info(
        f"Clearing {len(to_clear_group)} group notification subscriptions and {len(to_clear_spot)} spot notification subscriptions."
    )
    to_clear = set(
        map(
            lambda subscription:
            f"[{subscription.id}] {remove_pii(subscription.email)}",
            to_clear_spot + to_clear_group))
    with transaction() as t:
        for group_sub in to_clear_group:
            t.delete(group_sub)
        for spot_sub in to_clear_spot:
            t.delete(spot_sub)
    logging.info(f"Cleared {to_clear}")
Esempio n. 3
0
def query_groups(s):
    # Get the new groups.
    groups_resp = s.get_groups()
    group_payload = None
    if groups_resp.status_code != 200:
        logging.error(
            f"Couldn't get groups -> {groups_resp.status_code}, {groups_resp.content}"
        )
    else:
        try:
            group_payload = groups_resp.json()["payload"]
        except (JSONDecodeError, KeyError):
            pass

    if group_payload:
        current_groups = EligibilityGroup.query.all()
        current_group_ids = set(map(attrgetter("item_id"), current_groups))
        new_groups = list(
            filter(lambda group: group["item_code"] not in current_group_ids,
                   group_payload))
        if new_groups:
            logging.info(f"Found new groups: {len(new_groups)}")
            with transaction() as t:
                for new_group in new_groups:
                    group = EligibilityGroup(new_group["item_code"],
                                             new_group["item_description_ui"])
                    t.add(group)
        else:
            logging.info("No new groups")
Esempio n. 4
0
def both_unsubscribe(secret):
    try:
        secret_bytes = unhexlify(secret)
    except Exception:
        abort(404)
    spot_subscription = SpotSubscription.query.filter_by(
        secret=secret_bytes).first()
    group_subscription = GroupSubscription.query.filter_by(
        secret=secret_bytes).first()
    if spot_subscription is not None or group_subscription is not None:
        with transaction() as t:
            if spot_subscription is not None:
                t.delete(spot_subscription)
            if group_subscription is not None:
                t.delete(group_subscription)
        return render_template(
            "ok.html.jinja2",
            msg=
            "Odber notifikácii bol úspešne zrušený a Váše osobné údaje (email) boli odstránené."
        )
    else:
        return render_template(
            "error.html.jinja2",
            error=
            "Odber notifikácii sa nenašiel, buď neexistuje alebo bol už zrušený."
        ), 404
Esempio n. 5
0
def clear_db_push(secret):
    secret_bytes = unhexlify(secret)
    to_clear_group = GroupSubscription.query.filter_by(
        secret=secret_bytes).first()
    to_clear_spot = SpotSubscription.query.filter_by(
        secret=secret_bytes).first()
    with transaction() as t:
        if to_clear_group:
            t.delete(to_clear_group)
        if to_clear_spot:
            t.delete(to_clear_spot)
    logging.info(f"Cleared expired PUSH subscription")
Esempio n. 6
0
def spot_confirm(secret):
    try:
        secret_bytes = unhexlify(secret)
    except Exception:
        abort(404)
    subscription = SpotSubscription.query.filter_by(
        secret=secret_bytes).first_or_404()
    with transaction():
        subscription.status = Status.CONFIRMED
    if "push" in request.args:
        return jsonify({"msg": "Odber notifikácii bol potvrdený."})
    return render_template("ok.html.jinja2",
                           msg="Odber notifikácii bol potvrdený.")
Esempio n. 7
0
def both_confirm(secret):
    try:
        secret_bytes = unhexlify(secret)
    except Exception:
        abort(404)
    spot_subscription = SpotSubscription.query.filter_by(
        secret=secret_bytes).first()
    group_subscription = GroupSubscription.query.filter_by(
        secret=secret_bytes).first()
    if spot_subscription is not None or group_subscription is not None:
        with transaction():
            if spot_subscription is not None:
                spot_subscription.status = Status.CONFIRMED
            if group_subscription is not None:
                group_subscription.status = Status.CONFIRMED
        if "push" in request.args:
            return jsonify({"msg": "Odber notifikácii bol potvrdený."})
        return render_template("ok.html.jinja2",
                               msg="Odber notifikácii bol potvrdený.")
    else:
        abort(404)
Esempio n. 8
0
def run():
    s = NCZI(RetrySession())

    with sentry_sdk.start_span(op="query", description="Query groups"):
        query_groups(s)
        time.sleep(current_app.config["QUERY_DELAY"])

    with sentry_sdk.start_span(op="query", description="Query places"):
        place_stats = query_places_aggregate(s)

    with sentry_sdk.start_span(op="query", description="Query stats"):
        sub_stats = compute_subscription_stats()
        now = datetime.now()
        with transaction() as t:
            t.add(VaccinationStats(now, **place_stats))
            t.add(SubscriptionStats(now, **sub_stats))

    with sentry_sdk.start_span(op="notify", description="Notify groups"):
        if current_app.config["NOTIFY_GROUPS"]:
            notify_groups()
    with sentry_sdk.start_span(op="notify", description="Notify places"):
        if current_app.config["NOTIFY_SPOTS"]:
            notify_spots()
Esempio n. 9
0
def notify_spots():
    # Send out the spot notifications.
    all_cities = VaccinationCity.query.all()
    all_places = VaccinationPlace.query.all()
    free_map = {city.id: city.free_online for city in all_cities}
    place_map = {
        place.id: (place.title, place.free)
        for place in all_places if place.online and place.free
    }

    now = datetime.now()
    spot_backoff_time = timedelta(
        seconds=current_app.config["SPOT_NOTIFICATION_BACKOFF"])

    spot_subs_push = SpotSubscription.query.filter(
        and_(
            SpotSubscription.status == Status.CONFIRMED,
            SpotSubscription.email.is_(None),
            or_(
                SpotSubscription.last_notification_at.is_(None),
                SpotSubscription.last_notification_at <
                now - spot_backoff_time))).all()
    to_send_push = []
    for subscription in spot_subs_push:
        free_cities = set(city for city in subscription.cities
                          if free_map[city.id])
        if free_cities != set(subscription.known_cities):
            city_diff = free_cities.difference(subscription.known_cities)
            send_notification = all(free_map[city.id] > 1
                                    for city in city_diff)
            sub_entry = {
                "subscription": subscription,
                "free_cities": free_cities,
                "send_notification": send_notification
            }
            if free_cities and send_notification:
                to_send_push.insert(0, sub_entry)
            else:
                to_send_push.append(sub_entry)
    for entry in to_send_push:
        subscription = entry['subscription']
        try:
            with transaction():
                if entry["free_cities"] and entry["send_notification"]:
                    logging.info(
                        f"Sending spot notification to [{subscription.id}].")
                    new_subscription_cities = {
                        city.name: free_map[city.id]
                        for city in entry["free_cities"]
                    }
                    push_notification_spot.delay(
                        json.loads(subscription.push_sub),
                        hexlify(subscription.secret).decode(),
                        new_subscription_cities)
                    subscription.last_notification_at = now
                subscription.known_cities = list(entry["free_cities"])
        except (ObjectDeletedError, StaleDataError) as e:
            logging.warn(f"Got some races: {e}")

    spot_subs_email = SpotSubscription.query.filter(
        and_(
            SpotSubscription.status == Status.CONFIRMED,
            SpotSubscription.push_sub.is_(None),
            or_(
                SpotSubscription.last_notification_at.is_(None),
                SpotSubscription.last_notification_at <
                now - spot_backoff_time))).all()

    to_send_email = []
    for subscription in spot_subs_email:
        free_cities = set(city for city in subscription.cities
                          if free_map[city.id])
        if free_cities != set(subscription.known_cities):
            city_diff = free_cities.difference(subscription.known_cities)
            send_notification = all(free_map[city.id] > 1
                                    for city in city_diff)
            sub_entry = {
                "subscription": subscription,
                "free_cities": free_cities,
                "send_notification": send_notification
            }
            if free_cities and send_notification:
                to_send_email.insert(0, sub_entry)
            else:
                to_send_email.append(sub_entry)
    for entry in to_send_email:
        subscription = entry['subscription']
        try:
            with transaction():
                if entry["free_cities"] and entry["send_notification"]:
                    logging.info(
                        f"Sending spot notification to [{subscription.id}].")
                    new_subscription_cities = {
                        city.name: {
                            "free":
                            free_map[city.id],
                            "places": [
                                place_map[place.id] for place in city.places
                                if place.id in place_map
                            ]
                        }
                        for city in entry["free_cities"]
                    }
                    email_notification_spot.delay(
                        entry['subscription'].email,
                        hexlify(subscription.secret).decode(),
                        new_subscription_cities)
                    subscription.last_notification_at = now
                subscription.known_cities = list(entry["free_cities"])
        except (ObjectDeletedError, StaleDataError) as e:
            logging.warn(f"Got some races: {e}")
Esempio n. 10
0
def query_places_aggregate(s):
    # Update the places and free spots using the aggregate API
    current_places = VaccinationPlace.query.all()
    current_cities = VaccinationCity.query.all()
    total_free = 0
    total_free_online = 0
    places_resp = s.get_places_full()
    places_payload = None
    if places_resp.status_code != 200:
        logging.error(
            f"Couldn't get full places -> {places_resp.status_code}, {places_resp.content}"
        )
    else:
        try:
            places_payload = places_resp.json()["payload"]
        except (JSONDecodeError, KeyError):
            pass

    if places_payload:
        # Add new cities if any.
        city_map = {city.name: city for city in current_cities}
        current_city_names: Set[str] = set(
            map(attrgetter("name"), current_cities))
        city_names: Set[str] = set(map(itemgetter("city"), places_payload))
        new_city_names = city_names - current_city_names
        if new_city_names:
            logging.info(f"Found some new cities: {new_city_names}")
            with transaction() as t:
                for city_name in new_city_names:
                    city = VaccinationCity(city_name)
                    t.add(city)
                    city_map[city_name] = city
        else:
            logging.info("No new cities")

        place_ids: Set[str] = set(map(itemgetter("id"), places_payload))
        place_map = {int(place["id"]): place for place in places_payload}
        current_place_nczi_ids: Set[int] = set(
            map(attrgetter("nczi_id"), current_places))
        # Update places to online.
        online_places: List[VaccinationPlace] = list(
            filter(lambda place: str(place.nczi_id) in place_ids,
                   current_places))
        with transaction():
            for online_place in online_places:
                nczi_place = place_map[online_place.nczi_id]
                online_place.title = nczi_place["title"]
                online_place.longitude = float(nczi_place["longitude"])
                online_place.latitude = float(nczi_place["latitude"])
                online_place.city = city_map[nczi_place["city"]]
                online_place.street_name = nczi_place["street_name"]
                online_place.street_number = nczi_place["street_number"]
                online_place.online = True

        # Add new places.
        new_places = list(
            filter(
                lambda place: int(place["id"]) not in current_place_nczi_ids,
                places_payload))
        if new_places:
            logging.info(f"Found new places: {len(new_places)}")
            with transaction() as t:
                for new_place in new_places:
                    place = VaccinationPlace(int(new_place["id"]),
                                             new_place["title"],
                                             float(new_place["longitude"]),
                                             float(new_place["latitude"]),
                                             city_map[new_place["city"]],
                                             new_place["street_name"],
                                             new_place["street_number"], True,
                                             0)
                    t.add(place)
        else:
            logging.info("No new places")

        # Set places to offline.
        offline_places = list(
            filter(
                lambda place: str(place.nczi_id) not in place_ids and place.
                online, current_places))
        if offline_places:
            logging.info(
                f"Found some places that are now offline: {len(offline_places)}"
            )
            with transaction():
                for off_place in offline_places:
                    off_place.online = False
        else:
            logging.info("All current places are online")
        current_cities = VaccinationCity.query.options(
            selectinload(VaccinationCity.places)).all()
        current_places = VaccinationPlace.query.options(
            selectinload(VaccinationPlace.days)).all()
        for place in current_places:
            if place.nczi_id not in place_map:
                continue
            free_payload = place_map[place.nczi_id]["calendar_data"]
            free = 0
            days = []
            previous_days = {day.date: day for day in place.days}
            for line in free_payload:
                day_date = date.fromisoformat(line["c_date"])
                open = line["is_closed"] != 1
                try:
                    capacity = int(line["free_capacity"])
                except Exception:
                    capacity = 0
                day = previous_days.get(day_date)
                if day is None:
                    day = VaccinationDay(day_date, open, capacity, place)
                day.open = open
                day.capacity = capacity
                days.append(day)
                if capacity > 0 and open:
                    free += capacity
            if free:
                total_free += free
                total_free_online += free if place.online else 0
                logging.info(
                    f"Found free spots: {free} at {place.title} and they are {'online' if place.online else 'offline'}"
                )
            deleted_days = set(previous_days.values()) - set(days)
            with transaction() as t:
                if deleted_days:
                    for deleted_day in deleted_days:
                        t.delete(deleted_day)
                place.days = days
                place.free = free

    logging.info(
        f"Total free spots (online): {total_free} ({total_free_online})")
    total_places = len(current_places)
    online_places = len(list(filter(attrgetter("online"), current_places)))
    online_cities = len(
        list(
            filter(lambda city: any(place.online for place in city.places),
                   current_cities)))
    total_cities = len(current_cities)
    logging.info(f"Total places (online): {total_places} ({online_places})")
    return {
        "total_free_spots": total_free,
        "total_free_online_spots": total_free_online,
        "total_places": total_places,
        "online_places": online_places,
        "total_cities": total_cities,
        "online_cities": online_cities
    }