From 3ea6abd7f9b81a23649e742ddf2016f5d4563ca5 Mon Sep 17 00:00:00 2001 From: Liam Steckler Date: Sun, 14 Jan 2024 12:25:35 -0800 Subject: [PATCH 1/4] Exclude config.yml --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 3beed34..1da2241 100644 --- a/.gitignore +++ b/.gitignore @@ -166,3 +166,5 @@ cython_debug/ *.secret *.db .DS_Store + +config.yml \ No newline at end of file From d4a4d5a18cab8c22666db2975d3849f75d3648ea Mon Sep 17 00:00:00 2001 From: Liam Steckler Date: Sun, 14 Jan 2024 12:25:56 -0800 Subject: [PATCH 2/4] Update requirements --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index cb414f7..02c2af3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -beautifulsoup4==4.12.2 blurhash==1.1.4 certifi==2023.11.17 charset-normalizer==3.3.2 @@ -9,9 +8,9 @@ Mastodon.py==1.8.1 pillow==10.2.0 python-dateutil==2.8.2 python-magic==0.4.27 +PyYAML==6.0.1 requests==2.28.2 six==1.16.0 -soupsieve==2.5 SQLAlchemy==2.0.25 staticmap==0.5.7 typing_extensions==4.9.0 From 03f05fe44cefb8e0d4b1289dd47ffd921af7c449 Mon Sep 17 00:00:00 2001 From: Liam Steckler Date: Sun, 14 Jan 2024 12:27:02 -0800 Subject: [PATCH 3/4] Add reverse geocoding --- scl.py | 134 ++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 21 deletions(-) diff --git a/scl.py b/scl.py index 7bbc87a..3514a0a 100644 --- a/scl.py +++ b/scl.py @@ -1,8 +1,10 @@ import io +import math from datetime import datetime from typing import Optional import requests +import yaml from mastodon import Mastodon from PIL import Image, ImageDraw, ImageFont from sqlalchemy import create_engine, select @@ -20,11 +22,15 @@ except requests.JSONDecodeError: print("JSON could not be loaded from SCL API") raise -mastodon = Mastodon(access_token="scl_bot_mastodon.secret") - -with open("stadiamaps_api_key.secret", "r+") as stadiamaps_api_key_file: - # Reading from a file - stadiamaps_api_key = stadiamaps_api_key_file.read() +config = yaml.safe_load(open("config.yml")) +stadiamaps_api_key = config["stadiamaps"]["api_key"] +nominatim_url = config["nominatim"]["api_base_url"] +mastodon = Mastodon( + client_id=config["mastodon"]["client_id"], + client_secret=config["mastodon"]["client_secret"], + access_token=config["mastodon"]["access_token"], + api_base_url=config["mastodon"]["api_base_url"], +) class AttribStaticMap(StaticMap, object): @@ -181,22 +187,7 @@ with Session(engine) as session: # If the outage becomes medium/large, it'll then be posted as a new outage on the next run print("Outage is small, will not post") continue - print("Existing record not found") - post_text = """Seattle City Light is reporting a {} outage in {}. - -Start Date: {} -Est. Restoration: {} -Cause: {} - -{}""".format( - outage_size.lower(), - event["city"], - start_time.strftime(post_datetime_format), - estimated_restoration_time.strftime(post_datetime_format), - event["cause"], - hashtag_string, - ) try: map = AttribStaticMap( @@ -212,10 +203,92 @@ Cause: {} ) map.add_polygon(polygon) map_image = map.render() + + try: + + def num2deg(xtile, ytile, zoom): + n = 1 << zoom + lon_deg = xtile / n * 360.0 - 180.0 + lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) + lat_deg = math.degrees(lat_rad) + return lat_deg, lon_deg + + center_lat_lon = num2deg(map.x_center, map.y_center, map.zoom) + + # Zoom level 17 ensures that we won't get any building/POI names, just street names + geocode_url = "{nominatim_url}/reverse?lat={lat}&lon={lon}&format=geocodejson&zoom=17".format( + nominatim_url=nominatim_url, + lat=center_lat_lon[0], + lon=center_lat_lon[1], + ) + geocode_headers = {"User-Agent": "seattlecitylight-mastodon-bot"} + geocode_response = requests.get( + geocode_url, headers=geocode_headers + ) + try: + geocode = geocode_response.json() + except requests.JSONDecodeError: + print("JSON could not be loaded from nominatim API") + raise + + if ( + geocode["features"][0]["properties"]["geocoding"]["city"] + != "Seattle" + ): + city_not_seattle_text = " of {}".format( + geocode["features"][0]["properties"]["geocoding"]["city"] + ) + else: + city_not_seattle_text = "" + + street = geocode["features"][0]["properties"]["geocoding"]["name"] + + if "locality" in geocode["features"][0]["properties"]["geocoding"]: + locality = geocode["features"][0]["properties"]["geocoding"] + if locality == "Uptown": + locality = "Lower Queen Anne" + + alt_text = "A map showing the location of the outage, centered around {} in the {} area{}.".format( + street, + locality, + city_not_seattle_text, + ) + area_text = "the {} area{}".format( + locality, city_not_seattle_text + ) + elif ( + "district" in geocode["features"][0]["properties"]["geocoding"] + ): + alt_text = "A map showing the location of the outage, centered around {} in the {} area{}.".format( + street, + geocode["features"][0]["properties"]["geocoding"][ + "district" + ], + city_not_seattle_text, + ) + area_text = "the {} area{}".format( + geocode["features"][0]["properties"]["geocoding"][ + "district" + ], + city_not_seattle_text, + ) + else: + alt_text = "A map showing the location of the outage, centered around {} in {}.".format( + street, + geocode["features"][0]["properties"]["geocoding"]["city"], + ) + area_text = geocode["features"][0]["properties"]["geocoding"][ + "city" + ] + except Exception: + alt_text = "A map showing the location of the outage." + with io.BytesIO() as map_image_file: map_image.save(map_image_file, format="PNG", optimize=True) map_media_post = mastodon.media_post( - map_image_file.getvalue(), mime_type="image/png" + map_image_file.getvalue(), + mime_type="image/png", + description=alt_text, ) except Exception as e: @@ -225,6 +298,25 @@ Cause: {} ) map_media_post = None + # Fallback location from the SCL API in case one couldn't be reverse geocoded + if not area_text: + area_text = event["city"] + + post_text = """Seattle City Light is reporting a {} outage in {}. + +Start Date: {} +Est. Restoration: {} +Cause: {} + +{}""".format( + outage_size.lower(), + area_text, + start_time.strftime(post_datetime_format), + estimated_restoration_time.strftime(post_datetime_format), + event["cause"], + hashtag_string, + ) + mastodon_post_result = mastodon.status_post( status=post_text, media_ids=map_media_post, From eaa32c6987eac17e0ff0c95a5fe1299f21df286c Mon Sep 17 00:00:00 2001 From: Liam Steckler Date: Sun, 14 Jan 2024 12:34:15 -0800 Subject: [PATCH 4/4] Add sample config file --- config-sample.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 config-sample.yml diff --git a/config-sample.yml b/config-sample.yml new file mode 100644 index 0000000..44addea --- /dev/null +++ b/config-sample.yml @@ -0,0 +1,9 @@ +stadiamaps: + api_key: +nominatim: + api_base_url: +mastodon: + client_id: + client_secret: + access_token: + api_base_url: