Ruby-Cogs/onthisday/__init__.py
2025-04-02 22:57:51 -04:00

309 lines
9.9 KiB
Python

"""The OnThisDay module. Find out what happened on a certain day, in multiple different years in history."""
import asyncio
import datetime
import re
from random import choice
from typing import Dict, Iterable, Optional, Union
import aiohttp
import dateparser
import discord
from redbot.core import commands
from redbot.core.bot import Red
from redbot.core.commands import BadArgument, Context, Converter
from redbot.core.utils import get_end_user_data_statement
from redbot.core.utils.chat_formatting import inline, warning
__red_end_user_data_statement__ = get_end_user_data_statement(__file__)
ENDPOINT = "https://byabbe.se/on-this-day/{}/events.json"
DEFAULT_DESCRIPTION = """
Please send a valid year from the list of years below.
These are the most significant years for events occuring
on this day. Only years above 0 are included.
"""
MONTH_MAPPING = {
"1": "january",
"2": "february",
"3": "march",
"4": "april",
"5": "may",
"6": "june",
"7": "july",
"8": "august",
"9": "september",
"10": "october",
"11": "november",
"12": "december",
}
def highlight_numerical_data(string):
return re.sub(r"(([\d{1}],?)+)", r"**\1**", string)
def retrieve_above_0(year):
return year.strip().isdigit()
def columns(years):
m = f"{chr(12644)}\n" # padding
f = lambda s: inline(s.zfill(4))
for x in range(6, len(years), 6):
m += "\n" + (" " * 5).join(map(f, years[x - 6 : x]))
return m
def date_suffix(number) -> str:
number = int(number)
suffixes = {0: "th", 1: "st", 2: "nd", 3: "rd"}
for i in range(4, 10):
suffixes[i] = "th"
return str(number) + suffixes[int(str(number)[-1])]
def now() -> datetime.datetime:
return datetime.datetime.now()
def current_year() -> int:
return datetime.date.today().year
def yield_chunks(l, n):
for i in range(0, len(l), n):
yield l[i : i + n]
class DateConverter(Converter):
"""Date converter which uses dateparser.parse()."""
async def convert(self, ctx: Context, arg: str) -> datetime.datetime:
parsed = dateparser.parse(arg)
if parsed is None:
raise BadArgument("Unrecognized date/time.")
if parsed.strftime("%Y") != str(now().strftime("%Y")):
raise BadArgument("Please do not specify a year.")
return parsed
class YearDropdown(discord.ui.Select):
def __init__(self, otd: "OnThisDay", /):
self.otd = otd
cy = current_year()
options = [
discord.SelectOption(
label=year,
description=f"On this day, {cy - int(year)} years ago",
emoji="\N{CLOCK FACE THREE OCLOCK}",
)
for year in self.otd.year_data
if int(year) in self.otd.year_range
]
super().__init__(placeholder="Choose a year...", options=options)
async def callback(self, interaction: discord.Interaction):
await self.otd.display_events(
interaction,
year=self.values[0],
)
class YearDropdownView(discord.ui.View):
def __init__(self, otd: "OnThisDay"):
super().__init__()
self.add_item(YearDropdown(otd))
class YearRangeDropdown(discord.ui.Select):
"""This exists because the number of years dispatched always
exceeds the Select options cap of 25.
"""
YEAR_RANGES = [
range(1901),
range(1901, 1951),
range(1951, 2001),
range(2001, current_year() + 1),
]
YEAR_RANGES_HUMANIZED = ["0 - 1900", "1901 - 1950", "1951 - 2000", "2001 - present"]
def __init__(self, otd: "OnThisDay", /):
self.otd = otd
emojis = [
"\N{LARGE RED CIRCLE}",
"\N{LARGE ORANGE CIRCLE}",
"\N{LARGE YELLOW CIRCLE}",
"\N{LARGE GREEN CIRCLE}",
]
options = [
discord.SelectOption(
label=self.YEAR_RANGES_HUMANIZED[i],
value=i,
description=f"Get choice from {self.YEAR_RANGES_HUMANIZED[i].replace('-', 'to')}",
emoji=emojis[i],
)
for i in range(4)
]
super().__init__(placeholder="Choose a year range...", options=options)
async def callback(self, interaction: discord.Interaction):
self.otd.year_range = self.YEAR_RANGES[int(self.values[0])]
await interaction.response.edit_message(
content=f"Selected year range: **{self.YEAR_RANGES_HUMANIZED[int(self.values[0])]}**\nSelect a year from the list of available years.",
view=YearDropdownView(self.otd),
)
class YearRangeDropdownView(discord.ui.View):
def __init__(self, otd: "OnThisDay"):
super().__init__()
self.add_item(YearRangeDropdown(otd))
class ButtonView(discord.ui.View):
def __init__(self, buttons: dict[str, str]):
super().__init__()
for label, url in buttons.items():
self.add_item(discord.ui.Button(label=label, url=url))
class OnThisDay(commands.Cog):
"""Find out what happened on a certain day, in multiple different years in history."""
__version__ = "2.0.0"
__author__ = "Kreusada"
def __init__(self, bot: Red):
self.bot = bot
self.session = aiohttp.ClientSession()
self.date_number: Optional[str] = None
self.month_number: Optional[str] = None
self.year: Optional[str] = None
self.year_data = {}
self.year_range: Optional[range] = None
self.date_wiki: Optional[str] = None
def format_help_for_context(self, ctx: commands.Context) -> str:
context = super().format_help_for_context(ctx)
return f"{context}\n\nAuthor: {self.__author__}\nVersion: {self.__version__}"
async def red_delete_data_for_user(self, **kwargs):
"""Nothing to delete"""
return
async def cog_unload(self) -> None:
await self.session.close()
async def display_events(
self,
ctx: Union[commands.Context, discord.Interaction],
*,
year: str,
):
event = self.year_data[year]
years_ago = int(self.year) - int("".join(filter(str.isdigit, year)))
content = (
event["content"]
+ "\n\n"
+ f"This event occured on the __{date_suffix(self.date_number)} of {MONTH_MAPPING[self.month_number].capitalize()}, {year}__."
)
embed = discord.Embed(
title=f"On this day, {years_ago} years ago...",
description=highlight_numerical_data(content),
color=await self.bot.get_embed_colour(ctx.channel),
)
embed.set_footer(text="See the below links for related wikipedia articles")
_d = {
f"The year '{year}'": "https://en.wikipedia.org/wiki/" + year,
f"The date '{self.date_number.zfill(2)}/{self.month_number.zfill(2)}'": self.date_wiki,
}
embed.add_field(
name="Other significant events",
value="\n".join(f"- [{k}]({v})" for k, v in _d.items()),
inline=False,
)
wiki_dict = {w["title"]: w["wikipedia"] for w in event["wikipedia"]}
if isinstance(ctx, discord.Interaction):
send_method = ctx.response.edit_message
else:
send_method = ctx.send
await send_method(content=None, embed=embed, view=ButtonView(wiki_dict))
async def run_otd(
self,
ctx: commands.Context,
date: Optional[datetime.datetime],
random: bool = False,
):
self.cache_date(date)
try:
async with self.session.get(
ENDPOINT.format(f"{self.month_number}/{self.date_number}")
) as session:
if session.status != 200:
return await ctx.maybe_send_embed(
warning("An error occured whilst retrieving information for this day.")
)
content = await session.json()
except aiohttp.ClientError:
return await ctx.maybe_send_embed(
warning("An error occured whilst retrieving information for this day.")
)
else:
self.date_wiki = content["wikipedia"]
events = filter(lambda x: retrieve_above_0(x["year"]), content["events"])
data = {
e["year"]: {"content": e["description"], "wikipedia": e["wikipedia"]}
for e in events
}
self.year_data = data
if random:
await self.display_events(ctx, year=choice(list(data.keys())))
else:
await ctx.send(
"Choose a year range to select from:",
view=YearRangeDropdownView(self),
)
def cache_date(self, date: Optional[datetime.datetime], /) -> Dict[str, int]:
if date is None:
date = now()
self.month_number = date.strftime(r"%m").lstrip("0")
self.date_number = date.strftime(r"%d").lstrip("0")
self.year = date.strftime(r"%Y")
@commands.has_permissions(embed_links=True)
@commands.group(invoke_without_command=True, aliases=["otd"])
async def onthisday(self, ctx: commands.Context, *, date: DateConverter = None):
"""Find out what happened on this day, in various different years!
If you want to specify your own date, you can do so by using
`[p]onthisday [date]`.
You can also receive a random year by using `[p]onthisday random [day]`.
"""
await ctx.typing()
await self.run_otd(ctx, date=date, random=False)
@onthisday.command()
async def random(self, ctx: commands.Context, *, date: DateConverter = None):
"""Find out what happened on this day, in a random year.
If you want to specify your own date, you can do so by using
`[p]onthisday [date]`.
"""
await ctx.typing()
await self.run_otd(ctx, date=date, random=True)
async def setup(bot: Red):
await bot.add_cog(OnThisDay(bot))