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}")
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}")
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")
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
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")
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ý.")
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)
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()
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}")
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 }