Compare commits

...

4 commits

Author SHA1 Message Date
eaa32c6987 Add sample config file 2024-01-14 12:34:15 -08:00
03f05fe44c Add reverse geocoding 2024-01-14 12:27:02 -08:00
d4a4d5a18c Update requirements 2024-01-14 12:25:56 -08:00
3ea6abd7f9 Exclude config.yml 2024-01-14 12:25:35 -08:00
4 changed files with 125 additions and 23 deletions

2
.gitignore vendored
View file

@ -166,3 +166,5 @@ cython_debug/
*.secret *.secret
*.db *.db
.DS_Store .DS_Store
config.yml

9
config-sample.yml Normal file
View file

@ -0,0 +1,9 @@
stadiamaps:
api_key:
nominatim:
api_base_url:
mastodon:
client_id:
client_secret:
access_token:
api_base_url:

View file

@ -1,4 +1,3 @@
beautifulsoup4==4.12.2
blurhash==1.1.4 blurhash==1.1.4
certifi==2023.11.17 certifi==2023.11.17
charset-normalizer==3.3.2 charset-normalizer==3.3.2
@ -9,9 +8,9 @@ Mastodon.py==1.8.1
pillow==10.2.0 pillow==10.2.0
python-dateutil==2.8.2 python-dateutil==2.8.2
python-magic==0.4.27 python-magic==0.4.27
PyYAML==6.0.1
requests==2.28.2 requests==2.28.2
six==1.16.0 six==1.16.0
soupsieve==2.5
SQLAlchemy==2.0.25 SQLAlchemy==2.0.25
staticmap==0.5.7 staticmap==0.5.7
typing_extensions==4.9.0 typing_extensions==4.9.0

134
scl.py
View file

@ -1,8 +1,10 @@
import io import io
import math
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
import requests import requests
import yaml
from mastodon import Mastodon from mastodon import Mastodon
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from sqlalchemy import create_engine, select from sqlalchemy import create_engine, select
@ -20,11 +22,15 @@ except requests.JSONDecodeError:
print("JSON could not be loaded from SCL API") print("JSON could not be loaded from SCL API")
raise raise
mastodon = Mastodon(access_token="scl_bot_mastodon.secret") config = yaml.safe_load(open("config.yml"))
stadiamaps_api_key = config["stadiamaps"]["api_key"]
with open("stadiamaps_api_key.secret", "r+") as stadiamaps_api_key_file: nominatim_url = config["nominatim"]["api_base_url"]
# Reading from a file mastodon = Mastodon(
stadiamaps_api_key = stadiamaps_api_key_file.read() 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): 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 # 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") print("Outage is small, will not post")
continue continue
print("Existing record not found") 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: try:
map = AttribStaticMap( map = AttribStaticMap(
@ -212,10 +203,92 @@ Cause: {}
) )
map.add_polygon(polygon) map.add_polygon(polygon)
map_image = map.render() 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: with io.BytesIO() as map_image_file:
map_image.save(map_image_file, format="PNG", optimize=True) map_image.save(map_image_file, format="PNG", optimize=True)
map_media_post = mastodon.media_post( 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: except Exception as e:
@ -225,6 +298,25 @@ Cause: {}
) )
map_media_post = None 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( mastodon_post_result = mastodon.status_post(
status=post_text, status=post_text,
media_ids=map_media_post, media_ids=map_media_post,