from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
from typing import (
Dict,
List,
Optional,
Any
)
import discord
import pytz
from discord.ext import commands
from .Base import DatabaseChecker
[docs]class PartialBirthdayMember:
def __init__(self, member: discord.Member, birthday_date: datetime, timezone: str):
self.member = member
self.birthday_date = birthday_date
self.timezone = timezone
[docs]class BirthdayMember:
def __init__(self, birthday_manager: BirthdayManager, member: discord.Member):
self.birthday_manager = birthday_manager
self.member = member
self.table = self.birthday_manager.tables['birthdays']
@property
def __checks(self) -> Dict[str, int]:
return {'guild': self.member.guild.id, 'member': self.member.id}
[docs] async def birthday_date(self) -> datetime:
birthday_data = await self.birthday_manager.database.select(self.table, ["utc_birthday"], self.__checks)
return datetime.utcfromtimestamp(birthday_data["utc_birthday"])
[docs] async def timezone(self) -> str:
timezone_data = await self.birthday_manager.database.select(self.table, ["timezone"], self.__checks)
return timezone_data["timezone"]
[docs] async def delete(self) -> PartialBirthdayMember:
partial = PartialBirthdayMember(self.member, await self.birthday_date(), await self.timezone())
await self.birthday_manager.database.delete(self.table, self.__checks)
return partial
[docs] async def set_birthday_date(self, timestamp: float) -> None:
await self.birthday_manager.database.update(self.table, {"utc_birthday": timestamp}, self.__checks)
[docs] async def set_timezone(self, timezone: str) -> None:
await self.birthday_manager.database.update(self.table, {"timezone": timezone}, self.__checks)
[docs] async def age(self) -> int:
current_birthday = await self.birthday_date()
current_datetime = datetime.now()
return current_datetime.year - current_birthday.year - (datetime(year=current_datetime.year,
month=current_birthday.month,
day=current_birthday.day) >= current_datetime)
[docs]class BirthdayManager(DatabaseChecker):
def __init__(self, bot: commands.Bot):
super().__init__([
{'guild': 'snowflake', 'member': 'snowflake', 'utc_birthday': 'snowflake', 'timezone': 'string'}
], ['birthdays'])
self.bot = bot
self.add_event(self.on_database_connect)
[docs] async def on_database_connect(self):
self.bot.loop.create_task(self.__detect_birthdays())
[docs] async def create_birthday(self,
member: discord.Member,
member_birthday: float,
timezone: str = "UTC") -> None:
self._check_database()
await self.database.insertifnotexists(self.tables['birthdays'],
dict(zip(self.tables_column_data[0],
[member.guild.id, member.id, member_birthday, timezone])),
{'guild': member.guild.id, 'member': member.id})
[docs] async def get_birthday(self, member: discord.Member) -> Optional[BirthdayMember]:
self._check_database()
member_data = await self.database.select(self.tables['birthdays'],
[],
{'guild': member.guild.id, 'member': member.id}, True)
if member_data:
return BirthdayMember(self, member)
return None
[docs] async def get_upcoming(self, guild: discord.Guild) -> List[BirthdayMember]:
self._check_database()
member_data = await self.database.select(self.tables['birthdays'], [], fetchall=True)
member_data_formatted = []
for member in member_data:
current_datetime = datetime.now(pytz.timezone(member["timezone"]))
member["utc_birthday"] = datetime.utcfromtimestamp(member["utc_birthday"])
new_date = member["utc_birthday"].replace(year=current_datetime.year)
if new_date.timestamp() - current_datetime.timestamp() < 0:
new_date = new_date.replace(year=current_datetime.year + 1)
member["utc_birthday"] = new_date
member_data_formatted.append(member)
birthdays = []
for birthday_member in sorted(member_data_formatted, key=lambda x: x["utc_birthday"]):
member = guild.get_member(birthday_member['member'])
if member:
birthdays.append(BirthdayMember(self, member))
return birthdays
[docs] @staticmethod
def get_midnight_timezones() -> List[str]:
"""
This method returns a list of timezones where the current time is 12 am.
:return:
"""
current_utc_time = datetime.utcnow()
utc_offset = -(current_utc_time.hour % 24)
minutes = 30 if current_utc_time.minute > 5 else 0
if minutes == 30:
utc_offset -= 1
checks = (
timedelta(hours=utc_offset, minutes=minutes),
timedelta(hours=24 - -utc_offset if current_utc_time.hour != 0 else 0, minutes=minutes)
)
return [tz.zone for tz in map(pytz.timezone, pytz.all_timezones_set)
if current_utc_time.astimezone(tz).utcoffset() in checks]
[docs] async def get_members_with_birthday(self, timezones: List[str]) -> List[Dict[str, Any]]:
"""
This function receives a list of timezones and returns a list of members that have birthdays in that date
and timezone.
:param timezones:
:return:
"""
result_members = []
registered_members = await self.database.select(self.tables['birthdays'], [], fetchall=True)
birthday_members = [x for x in registered_members if x["timezone"] in timezones]
for birthday_member in birthday_members:
timezone_time = datetime.now(pytz.timezone(birthday_member["timezone"]))
date_of_birth = datetime.fromtimestamp(birthday_member["utc_birthday"])
if date_of_birth.month == timezone_time.month and date_of_birth.day == timezone_time.day:
result_members.append(birthday_member)
return result_members
[docs] @staticmethod
def round_to_nearest(timedelta_to_round):
"""
This function receives a timedelta to round to and gets the amount of seconds before that timestamp.
:param timedelta_to_round:
:return:
"""
now = datetime.now()
nearest = now + (datetime.min - now) % timedelta_to_round
return nearest.timestamp() - now.timestamp()
async def __detect_birthdays(self) -> None:
await self.bot.wait_until_ready()
while not self.bot.is_closed():
await asyncio.sleep(self.round_to_nearest(timedelta(minutes=30)))
for birthday_member in await self.get_members_with_birthday(self.get_midnight_timezones()):
guild = self.bot.get_guild(birthday_member["guild"])
if guild:
member = guild.get_member(birthday_member["member"])
if member:
await self.call_event("on_member_birthday", BirthdayMember(
self, member
))