aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar kwzrd <[email protected]>2020-04-01 10:08:11 +0200
committerGravatar kwzrd <[email protected]>2020-04-01 10:08:11 +0200
commit04cc5bf617500af694cc27eee072265b31bd9a47 (patch)
treebe1eed54972d9843f66114311f93b68b579046ac
parentDocstring: remove dots from list entries (diff)
parentMerge pull request #382 from ks129/game-fuzzy (diff)
Merge master: adjust `Space` cog location
Module was placed in the `seasons` pkg, which we renamed to `exts`.
-rw-r--r--bot/constants.py1
-rw-r--r--bot/exts/evergreen/game.py37
-rw-r--r--bot/exts/evergreen/space.py240
3 files changed, 274 insertions, 4 deletions
diff --git a/bot/constants.py b/bot/constants.py
index 638448ea..ca9bb94a 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -181,6 +181,7 @@ class Tokens(NamedTuple):
omdb = environ.get("OMDB_API_KEY")
youtube = environ.get("YOUTUBE_API_KEY")
tmdb = environ.get("TMDB_API_KEY")
+ nasa = environ.get("NASA_API_KEY")
igdb = environ.get("IGDB_API_KEY")
github = environ.get("GITHUB_TOKEN")
diff --git a/bot/exts/evergreen/game.py b/bot/exts/evergreen/game.py
index d43b1ad6..3c8b2725 100644
--- a/bot/exts/evergreen/game.py
+++ b/bot/exts/evergreen/game.py
@@ -1,6 +1,7 @@
import difflib
import logging
import random
+import re
from datetime import datetime as dt
from enum import IntEnum
from typing import Any, Dict, List, Optional, Tuple
@@ -25,6 +26,8 @@ HEADERS = {
logger = logging.getLogger(__name__)
+REGEX_NON_ALPHABET = re.compile(r"[^a-z0-9]", re.IGNORECASE)
+
# ---------
# TEMPLATES
# ---------
@@ -135,7 +138,7 @@ class Games(Cog):
self.refresh_genres_task.start()
- @tasks.loop(hours=1.0)
+ @tasks.loop(hours=24.0)
async def refresh_genres_task(self) -> None:
"""Refresh genres in every hour."""
try:
@@ -194,9 +197,25 @@ class Games(Cog):
try:
games = await self.get_games_list(amount, self.genres[genre], offset=random.randint(0, 150))
except KeyError:
- possibilities = "`, `".join(difflib.get_close_matches(genre, self.genres))
- await ctx.send(f"Invalid genre `{genre}`. {f'Maybe you meant `{possibilities}`?' if possibilities else ''}")
- return
+ possibilities = await self.get_best_results(genre)
+ # If there is more than 1 possibilities, show these.
+ # If there is only 1 possibility, use it as genre.
+ # Otherwise send message about invalid genre.
+ if len(possibilities) > 1:
+ display_possibilities = "`, `".join(p[1] for p in possibilities)
+ await ctx.send(
+ f"Invalid genre `{genre}`. "
+ f"{f'Maybe you meant `{display_possibilities}`?' if display_possibilities else ''}"
+ )
+ return
+ elif len(possibilities) == 1:
+ games = await self.get_games_list(
+ amount, self.genres[possibilities[0][1]], offset=random.randint(0, 150)
+ )
+ genre = possibilities[0][1]
+ else:
+ await ctx.send(f"Invalid genre `{genre}`.")
+ return
# Create pages and paginate
pages = [await self.create_page(game) for game in games]
@@ -385,6 +404,16 @@ class Games(Cog):
return page, url
+ async def get_best_results(self, query: str) -> List[Tuple[float, str]]:
+ """Get best match result of genre when original genre is invalid."""
+ results = []
+ for genre in self.genres:
+ ratios = [difflib.SequenceMatcher(None, query, genre).ratio()]
+ for word in REGEX_NON_ALPHABET.split(genre):
+ ratios.append(difflib.SequenceMatcher(None, query, word).ratio())
+ results.append((round(max(ratios), 2), genre))
+ return sorted((item for item in results if item[0] >= 0.60), reverse=True)[:4]
+
def setup(bot: SeasonalBot) -> None:
"""Add/Load Games cog."""
diff --git a/bot/exts/evergreen/space.py b/bot/exts/evergreen/space.py
new file mode 100644
index 00000000..89b31e87
--- /dev/null
+++ b/bot/exts/evergreen/space.py
@@ -0,0 +1,240 @@
+import logging
+import random
+from datetime import datetime
+from typing import Any, Dict, Optional, Union
+from urllib.parse import urlencode
+
+from discord import Embed
+from discord.ext import tasks
+from discord.ext.commands import BadArgument, Cog, Context, Converter, group
+
+from bot.bot import SeasonalBot
+from bot.constants import Tokens
+
+logger = logging.getLogger(__name__)
+
+NASA_BASE_URL = "https://api.nasa.gov"
+NASA_IMAGES_BASE_URL = "https://images-api.nasa.gov"
+NASA_EPIC_BASE_URL = "https://epic.gsfc.nasa.gov"
+
+
+class DateConverter(Converter):
+ """Parse SOL or earth date (in format YYYY-MM-DD) into `int` or `datetime`. When invalid input, raise error."""
+
+ async def convert(self, ctx: Context, argument: str) -> Union[int, datetime]:
+ """Parse date (SOL or earth) into `datetime` or `int`. When invalid value, raise error."""
+ if argument.isdigit():
+ return int(argument)
+ try:
+ date = datetime.strptime(argument, "%Y-%m-%d")
+ except ValueError:
+ raise BadArgument(f"Can't convert `{argument}` to `datetime` in format `YYYY-MM-DD` or `int` in SOL.")
+ return date
+
+
+class Space(Cog):
+ """Space Cog contains commands, that show images, facts or other information about space."""
+
+ def __init__(self, bot: SeasonalBot):
+ self.bot = bot
+ self.http_session = bot.http_session
+
+ self.rovers = {}
+ self.get_rovers.start()
+
+ def cog_unload(self) -> None:
+ """Cancel `get_rovers` task when Cog will unload."""
+ self.get_rovers.cancel()
+
+ @tasks.loop(hours=24)
+ async def get_rovers(self) -> None:
+ """Get listing of rovers from NASA API and info about their start and end dates."""
+ data = await self.fetch_from_nasa("mars-photos/api/v1/rovers")
+
+ for rover in data["rovers"]:
+ self.rovers[rover["name"].lower()] = {
+ "min_date": rover["landing_date"],
+ "max_date": rover["max_date"],
+ "max_sol": rover["max_sol"]
+ }
+
+ @group(name="space", invoke_without_command=True)
+ async def space(self, ctx: Context) -> None:
+ """Head command that contains commands about space."""
+ await ctx.send_help("space")
+
+ @space.command(name="apod")
+ async def apod(self, ctx: Context, date: Optional[str] = None) -> None:
+ """
+ Get Astronomy Picture of Day from NASA API. Date is optional parameter, what formatting is YYYY-MM-DD.
+
+ If date is not specified, this will get today APOD.
+ """
+ params = {}
+ # Parse date to params, when provided. Show error message when invalid formatting
+ if date:
+ try:
+ params["date"] = datetime.strptime(date, "%Y-%m-%d").date().isoformat()
+ except ValueError:
+ await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.")
+ return
+
+ result = await self.fetch_from_nasa("planetary/apod", params)
+
+ await ctx.send(
+ embed=self.create_nasa_embed(
+ f"Astronomy Picture of the Day - {result['date']}",
+ result["explanation"],
+ result["url"]
+ )
+ )
+
+ @space.command(name="nasa")
+ async def nasa(self, ctx: Context, *, search_term: Optional[str] = None) -> None:
+ """Get random NASA information/facts + image. Support `search_term` parameter for more specific search."""
+ params = {
+ "media_type": "image"
+ }
+ if search_term:
+ params["q"] = search_term
+
+ # Don't use API key, no need for this.
+ data = await self.fetch_from_nasa("search", params, NASA_IMAGES_BASE_URL, use_api_key=False)
+ if len(data["collection"]["items"]) == 0:
+ await ctx.send(f"Can't find any items with search term `{search_term}`.")
+ return
+
+ item = random.choice(data["collection"]["items"])
+
+ await ctx.send(
+ embed=self.create_nasa_embed(
+ item["data"][0]["title"],
+ item["data"][0]["description"],
+ item["links"][0]["href"]
+ )
+ )
+
+ @space.command(name="epic")
+ async def epic(self, ctx: Context, date: Optional[str] = None) -> None:
+ """Get one of latest random image of earth from NASA EPIC API. Support date parameter, format is YYYY-MM-DD."""
+ if date:
+ try:
+ show_date = datetime.strptime(date, "%Y-%m-%d").date().isoformat()
+ except ValueError:
+ await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.")
+ return
+ else:
+ show_date = None
+
+ # Don't use API key, no need for this.
+ data = await self.fetch_from_nasa(
+ f"api/natural{f'/date/{show_date}' if show_date else ''}",
+ base=NASA_EPIC_BASE_URL,
+ use_api_key=False
+ )
+ if len(data) < 1:
+ await ctx.send("Can't find any images in this date.")
+ return
+
+ item = random.choice(data)
+
+ year, month, day = item["date"].split(" ")[0].split("-")
+ image_url = f"{NASA_EPIC_BASE_URL}/archive/natural/{year}/{month}/{day}/jpg/{item['image']}.jpg"
+
+ await ctx.send(
+ embed=self.create_nasa_embed(
+ "Earth Image", item["caption"], image_url, f" \u2022 Identifier: {item['identifier']}"
+ )
+ )
+
+ @space.group(name="mars", invoke_without_command=True)
+ async def mars(
+ self,
+ ctx: Context,
+ date: Optional[DateConverter] = None,
+ rover: Optional[str] = "curiosity"
+ ) -> None:
+ """
+ Get random Mars image by date. Support both SOL (martian solar day) and earth date and rovers.
+
+ Earth date formatting is YYYY-MM-DD. Use `.space mars dates` to get all currently available rovers.
+ """
+ rover = rover.lower()
+ if rover not in self.rovers:
+ await ctx.send(
+ (
+ f"Invalid rover `{rover}`.\n"
+ f"**Rovers:** `{'`, `'.join(f'{r.capitalize()}' for r in self.rovers)}`"
+ )
+ )
+ return
+
+ # When date not provided, get random SOL date between 0 and rover's max.
+ if date is None:
+ date = random.randint(0, self.rovers[rover]["max_sol"])
+
+ params = {}
+ if isinstance(date, int):
+ params["sol"] = date
+ else:
+ params["earth_date"] = date.date().isoformat()
+
+ result = await self.fetch_from_nasa(f"mars-photos/api/v1/rovers/{rover}/photos", params)
+ if len(result["photos"]) < 1:
+ err_msg = (
+ f"We can't find result in date "
+ f"{date.date().isoformat() if isinstance(date, datetime) else f'{date} SOL'}.\n"
+ f"**Note:** Dates must match with rover's working dates. Please use `{ctx.prefix}space mars dates` to "
+ "see working dates for each rover."
+ )
+ await ctx.send(err_msg)
+ return
+
+ item = random.choice(result["photos"])
+ await ctx.send(
+ embed=self.create_nasa_embed(
+ f"{item['rover']['name']}'s {item['camera']['full_name']} Mars Image", "", item["img_src"],
+ )
+ )
+
+ @mars.command(name="dates", aliases=["d", "date", "rover", "rovers", "r"])
+ async def dates(self, ctx: Context) -> None:
+ """Get current available rovers photo date ranges."""
+ await ctx.send("\n".join(
+ f"**{r.capitalize()}:** {i['min_date']} **-** {i['max_date']}" for r, i in self.rovers.items()
+ ))
+
+ async def fetch_from_nasa(
+ self,
+ endpoint: str,
+ additional_params: Optional[Dict[str, Any]] = None,
+ base: Optional[str] = NASA_BASE_URL,
+ use_api_key: bool = True
+ ) -> Dict[str, Any]:
+ """Fetch information from NASA API, return result."""
+ params = {}
+ if use_api_key:
+ params["api_key"] = Tokens.nasa
+
+ # Add additional parameters to request parameters only when they provided by user
+ if additional_params is not None:
+ params.update(additional_params)
+
+ async with self.http_session.get(url=f"{base}/{endpoint}?{urlencode(params)}") as resp:
+ return await resp.json()
+
+ def create_nasa_embed(self, title: str, description: str, image: str, footer: Optional[str] = "") -> Embed:
+ """Generate NASA commands embeds. Required: title, description and image URL, footer (addition) is optional."""
+ return Embed(
+ title=title,
+ description=description
+ ).set_image(url=image).set_footer(text="Powered by NASA API" + footer)
+
+
+def setup(bot: SeasonalBot) -> None:
+ """Load Space Cog."""
+ if not Tokens.nasa:
+ logger.warning("Can't find NASA API key. Not loading Space Cog.")
+ return
+
+ bot.add_cog(Space(bot))