Discord-Bot-Python/cogs/weather.py

264 lines
10 KiB
Python
Raw Normal View History

2023-01-23 12:29:07 +00:00
import asyncio
import html
import json
import random
from time import time
from typing import Annotated, Any, Optional
from zoneinfo import ZoneInfo
import discord
from discord import app_commands
from discord.ext import commands
from discord.ext.commands import Context
from discord.utils import get
from discord.utils import *
from helpers import checks, db_manager
# This is the Weather module
class Weather(commands.Cog, name="weather"):
def __init__(self, bot):
self.bot = bot
with open("data/weather.json") as f:
self.weather_constants = json.load(f)
@commands.group()
async def weather(self, ctx: commands.Context):
"""Show current weather in given location"""
if ctx.invoked_subcommand is None:
await util.command_group_help(ctx)
else:
ctx.location = await self.bot.db.fetch_value(
"SELECT location_string FROM user_settings WHERE user_id = %s",
ctx.author.id,
)
@weather.command(name="now")
async def weather_now(self, ctx: commands.Context, *, location: Optional[str] = None):
if location is None:
location = ctx.location
if ctx.location is None:
raise exceptions.CommandInfo(
f"Please save your location using `{ctx.prefix}weather save <location...>`"
)
lat, lon, address = await self.geolocate(location)
local_time, country_code = await self.get_country_information(lat, lon)
API_BASE_URL = "https://api.tomorrow.io/v4/timelines"
params = {
"apikey": self.bot.keychain.TOMORROWIO_TOKEN,
"location": f"{lat},{lon}",
"fields": ",".join(
[
"precipitationProbability",
"precipitationType",
"windSpeed",
"windGust",
"windDirection",
"temperature",
"temperatureApparent",
"cloudCover",
"weatherCode",
"humidity",
"temperatureMin",
"temperatureMax",
"sunriseTime",
"sunsetTime",
]
),
"units": "metric",
"timesteps": ",".join(["current", "1d"]),
"endTime": arrow.utcnow().shift(days=+1, minutes=+5).isoformat(),
}
if isinstance(local_time.tzinfo, ZoneInfo):
params["timezone"] = str(local_time.tzinfo)
else:
logger.warning("Arrow object must be constructed with ZoneInfo timezone object")
async with self.bot.session.get(API_BASE_URL, params=params) as response:
if response.status != 200:
logger.error(response.status)
logger.error(response.headers)
logger.error(await response.text())
raise exceptions.CommandError(f"Weather api returned HTTP ERROR {response.status}")
data = await response.json(loads=orjson.loads)
current_data = next(
filter(lambda t: t["timestep"] == "current", data["data"]["timelines"])
)
daily_data = next(filter(lambda t: t["timestep"] == "1d", data["data"]["timelines"]))
values_current = current_data["intervals"][0]["values"]
values_today = daily_data["intervals"][0]["values"]
# values_tomorrow = daily_data["intervals"][1]["values"]
temperature = values_current["temperature"]
temperature_apparent = values_current["temperatureApparent"]
sunrise = arrow.get(values_current["sunriseTime"]).to(local_time.tzinfo).format("HH:mm")
sunset = arrow.get(values_current["sunsetTime"]).to(local_time.tzinfo).format("HH:mm")
icon = self.weather_constants["id_to_icon"][str(values_current["weatherCode"])]
summary = self.weather_constants["id_to_description"][str(values_current["weatherCode"])]
if (
values_today["precipitationType"] != 0
and values_today["precipitationProbability"] != 0
):
precipitation_type = self.weather_constants["precipitation"][
str(values_today["precipitationType"])
]
summary += f", with {values_today['precipitationProbability']}% chance of {precipitation_type}"
content = discord.Embed(color=int("e1e8ed", 16))
content.title = f":flag_{country_code.lower()}: {address}"
content.set_footer(text=f"🕐 Local time {local_time.format('HH:mm')}")
def render(F: bool):
information_rows = [
f":thermometer: Currently **{temp(temperature, F)}**, feels like **{temp(temperature_apparent, F)}**",
f":calendar: Daily low **{temp(values_today['temperatureMin'], F)}**, high **{temp(values_today['temperatureMax'], F)}**",
f":dash: Wind speed **{values_current['windSpeed']} m/s** with gusts of **{values_current['windGust']} m/s**",
f":sunrise: Sunrise at **{sunrise}**, sunset at **{sunset}**",
f":sweat_drops: Air humidity **{values_current['humidity']}%**",
f":map: [See on map](https://www.google.com/maps/search/?api=1&query={lat},{lon})",
]
content.clear_fields().add_field(
name=f"{icon} {summary}",
value="\n".join(information_rows),
)
return content
await WeatherUnitToggler(render, False).run(ctx)
@weather.command(name="forecast")
async def weather_forecast(self, ctx: commands.Context, *, location: Optional[str] = None):
if location is None:
location = ctx.location
if ctx.location is None:
raise exceptions.CommandInfo(
f"Please save your location using `{ctx.prefix}weather save <location...>`"
)
lat, lon, address = await self.geolocate(location)
local_time, country_code = await self.get_country_information(lat, lon)
API_BASE_URL = "https://api.tomorrow.io/v4/timelines"
params = {
"apikey": self.bot.keychain.TOMORROWIO_TOKEN,
"location": f"{lat},{lon}",
"fields": ",".join(
[
"precipitationProbability",
"precipitationType",
"windSpeed",
"windGust",
"windDirection",
"temperature",
"temperatureApparent",
"cloudCover",
"weatherCode",
"humidity",
"temperatureMin",
"temperatureMax",
]
),
"units": "metric",
"timesteps": "1d",
"endTime": arrow.utcnow().shift(days=+7).isoformat(),
}
if isinstance(local_time.tzinfo, ZoneInfo):
params["timezone"] = str(local_time.tzinfo)
else:
logger.warning("Arrow object must be constructed with ZoneInfo timezone object")
async with self.bot.session.get(API_BASE_URL, params=params) as response:
if response.status != 200:
logger.error(response.status)
logger.error(response.headers)
logger.error(await response.text())
raise exceptions.CommandError(f"Weather api returned HTTP ERROR {response.status}")
data = await response.json(loads=orjson.loads)
content = discord.Embed(
title=f":flag_{country_code.lower()}: {address}",
color=int("ffcc4d", 16),
)
def render(F: bool):
days = []
for day in data["data"]["timelines"][0]["intervals"]:
date = arrow.get(day["startTime"]).format("**`ddd`** `D/M`")
values = day["values"]
minTemp = values["temperatureMin"]
maxTemp = values["temperatureMax"]
icon = self.weather_constants["id_to_icon"][str(values["weatherCode"])]
description = self.weather_constants["id_to_description"][
str(values["weatherCode"])
]
days.append(
f"{date} {icon} **{temp(maxTemp, F)}** / **{temp(minTemp, F)}** — {description}"
)
content.description = "\n".join(days)
return content
await WeatherUnitToggler(render, False).run(ctx)
@weather.command(name="save")
async def weather_save(self, ctx: commands.Context, *, location: str):
await self.bot.db.execute(
"""
INSERT INTO user_settings (user_id, location_string)
VALUES (%s, %s)
ON DUPLICATE KEY UPDATE
location_string = VALUES(location_string)
""",
ctx.author.id,
location,
)
return await util.send_success(ctx, f"Saved your location as `{location}`")
async def geolocate(self, location):
GOOGLE_GEOCODING_API_URL = "https://maps.googleapis.com/maps/api/geocode/json"
params = {"address": location, "key": self.bot.keychain.GCS_DEVELOPER_KEY}
async with self.bot.session.get(GOOGLE_GEOCODING_API_URL, params=params) as response:
geocode_data = await response.json(loads=orjson.loads)
try:
geocode_data = geocode_data["results"][0]
except IndexError:
raise exceptions.CommandWarning("Could not find that location!")
formatted_name = geocode_data["formatted_address"]
lat = geocode_data["geometry"]["location"]["lat"]
lon = geocode_data["geometry"]["location"]["lng"]
return lat, lon, formatted_name
async def get_country_information(self, lat, lon):
TIMEZONE_API_URL = "http://api.timezonedb.com/v2.1/get-time-zone"
params = {
"key": self.bot.keychain.TIMEZONEDB_API_KEY,
"format": "json",
"by": "position",
"lat": lat,
"lng": lon,
}
async with self.bot.session.get(TIMEZONE_API_URL, params=params) as response:
data = await response.json(loads=orjson.loads)
country_code = data["countryCode"]
try:
local_time = arrow.now(ZoneInfo(data["zoneName"]))
except ValueError:
# does not have a time zone
# most likely a special place such as antarctica
# use UTC
local_time = arrow.utcnow()
return local_time, country_code
async def setup(bot):
await bot.add_cog(Weather(bot))