From 2385c7886feee80eb689901a12152bbf35dc0a83 Mon Sep 17 00:00:00 2001 From: PancakeSparkle Date: Tue, 21 Jan 2020 03:11:51 +0200 Subject: [PATCH] Add files via upload --- birthday/__init__.py | 6 + birthday/birthday.py | 260 +++++++++++++++++++++++++++++++++++++++++++ birthday/info.json | 21 ++++ 3 files changed, 287 insertions(+) create mode 100644 birthday/__init__.py create mode 100644 birthday/birthday.py create mode 100644 birthday/info.json diff --git a/birthday/__init__.py b/birthday/__init__.py new file mode 100644 index 0000000..c3a667d --- /dev/null +++ b/birthday/__init__.py @@ -0,0 +1,6 @@ + +from .birthday import Birthdays + + +def setup(bot): + bot.add_cog(Birthdays(bot)) diff --git a/birthday/birthday.py b/birthday/birthday.py new file mode 100644 index 0000000..a78a8f3 --- /dev/null +++ b/birthday/birthday.py @@ -0,0 +1,260 @@ +import logging +import hashlib +import asyncio +import contextlib +import datetime +import discord +import itertools + +from redbot.core import commands, Config, checks +from redbot.core.bot import Red +from redbot.core.config import Group +from redbot.core.i18n import Translator, cog_i18n +from redbot.core.commands import Context, Cog + +T_ = Translator("Birthdays", __file__) + +def _(s): + def func(*args, **kwargs): + real_args = list(args) + real_args.pop(0) + return T_(s).format(*real_args, **kwargs) + return func + +@cog_i18n(T_) +class Birthdays(Cog): + """Announces people's birthdays and gives them a birthday role for the whole day""" + __author__ = "PancakeSparkle#8243" + + # Just some constants + DATE_GROUP = "DATE" + GUILD_DATE_GROUP = "GUILD_DATE" + + # More constants + BDAY_LIST_TITLE = _("Birthday List") + + # Even more constants + BDAY_WITH_YEAR = _("<@!{}> is now **{} years old**. <:aureliahappy:548738609763713035>") + BDAY_WITHOUT_YEAR = _("Everypony say Happy Hirthday to <@!{}>! <:aureliahappy:548738609763713035>") + ROLE_SET = _("<:aureliaagree:616091883013144586> The birthday role on **{g}** has been set to: **{r}**.") + BDAY_INVALID = _(":x: The birthday date you entered is invalid. It must be `MM-DD`.") + BDAY_SET = _("<:aureliaagree:616091883013144586> Your birthday has been set to: **{}**.") + CHANNEL_SET = _("<:aureliaagree:616091883013144586> " + "The channel for announcing birthdays on **{g}** has been set to: **{c}**.") + BDAY_REMOVED = _(":put_litter_in_its_place: Your birthday has been removed.") + + + def __init__(self, bot): + super().__init__() + self.bot = bot + self.logger = logging.getLogger("aurelia.cogs.birthdays") + unique_id = int(hashlib.sha512((self.__author__ + "@" + self.__class__.__name__).encode()).hexdigest(), 16) + self.config = Config.get_conf(self, identifier=unique_id) + self.config.init_custom(self.DATE_GROUP, 1) + self.config.init_custom(self.GUILD_DATE_GROUP, 2) + self.config.register_guild(channel=None, role=None, yesterdays=[]) + self.bday_loop = asyncio.ensure_future(self.initialise()) + asyncio.ensure_future(self.check_breaking_change()) + + # Events + async def initialise(self): + await self.bot.wait_until_ready() + with contextlib.suppress(RuntimeError): + while self == self.bot.get_cog(self.__class__.__name__): + now = datetime.datetime.utcnow() + tomorrow = (now + datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + await asyncio.sleep((tomorrow - now).total_seconds()) + await self.clean_yesterday_bdays() + await self.do_today_bdays() + + def cog_unload(self): + self.bday_loop.cancel() + + # Commands + @commands.group() + @commands.guild_only() + async def bday(self, ctx: Context): + """Birthday settings""" + pass + + @bday.command(name="channel") + @checks.mod_or_permissions(manage_roles=True) + async def bday_channel(self, ctx: Context, channel: discord.TextChannel): + """Sets the birthday announcement channel""" + message = ctx.message + guild = message.guild + await self.config.guild(channel.guild).channel.set(channel.id) + await message.channel.send(self.CHANNEL_SET(g=guild.name, c=channel.name)) + + @bday.command(name="role") + @checks.mod_or_permissions(manage_roles=True) + async def bday_role(self, ctx: Context, role: discord.Role): + """Sets the birthday role""" + message = ctx.message + guild = message.guild + await self.config.guild(role.guild).role.set(role.id) + await message.channel.send(self.ROLE_SET(g=guild.name, r=role.name)) + + @bday.command(name="remove", aliases=["del", "clear", "rm"]) + async def bday_remove(self, ctx: Context): + """Unsets your birthday date""" + message = ctx.message + await self.remove_user_bday(message.guild.id, message.author.id) + await message.channel.send(self.BDAY_REMOVED()) + + @bday.command(name="set") + async def bday_set(self, ctx: Context, date, year: int=None): + """Sets your birthday date + + The given date must be given as: MM-DD + Year is optional. If not given, the age won't be displayed.""" + message = ctx.message + channel = message.channel + author = message.author + birthday = self.parse_date(date) + if birthday is None: + print(self.BDAY_INVALID()) + await channel.send(self.BDAY_INVALID()) + + else: + await self.remove_user_bday(message.guild.id, author.id) + await self.get_date_config(message.guild.id, birthday.toordinal()).get_attr(author.id).set(year) + bday_month_str = birthday.strftime("%B") + bday_day_str = birthday.strftime("%d").lstrip("0") + await channel.send(self.BDAY_SET(bday_month_str + " " + bday_day_str)) + + @bday.command(name="list") + async def bday_list(self, ctx: Context): + """Lists birthdays + + If a user has their year set, it will display the age they'll get after their birthday this year""" + message = ctx.message + await self.clean_bdays() + bdays = await self.get_guild_date_configs(message.guild.id) + this_year = datetime.date.today().year + embed = discord.Embed(title=self.BDAY_LIST_TITLE(), color=discord.Colour.lighter_grey()) + for k, g in itertools.groupby(sorted(datetime.datetime.fromordinal(int(o)) for o in bdays.keys()), + lambda i: i.month): + + value = "\n".join(date.strftime("%d").lstrip("0") + ": " + + ", ".join("<@!{}>".format(u_id) + + ("" if year is None else " ({})".format(this_year - int(year))) + for u_id, year in bdays.get(str(date.toordinal()), {}).items()) + for date in g if len(bdays.get(str(date.toordinal()))) > 0) + if not value.isspace(): + embed.add_field(name=datetime.datetime(year=1, month=k, day=1).strftime("%B"), value=value) + await message.channel.send(embed=embed) + + async def clean_bday(self, guild_id: int, guild_config: dict, user_id: int): + guild = self.bot.get_guild(guild_id) + if guild is not None: + role = discord.utils.get(guild.roles, id=guild_config.get("role")) + + await self.maybe_update_guild(guild) + member = guild.get_member(user_id) + if member is not None and role is not None and role in member.roles: + + await member.remove_roles(role) + + async def handle_bday(self, user_id: int, year: str): + embed = discord.Embed(color=discord.Colour.gold()) + if year is not None: + age = datetime.date.today().year - int(year) + embed.description = self.BDAY_WITH_YEAR(user_id, age) + else: + embed.description = self.BDAY_WITHOUT_YEAR(user_id) + all_guild_configs = await self.config.all_guilds() + for guild_id, guild_config in all_guild_configs.items(): + guild = self.bot.get_guild(guild_id) + if guild is not None: + member = guild.get_member(user_id) + if member is not None: + role_id = guild_config.get("role") + if role_id is not None: + role = discord.utils.get(guild.roles, id=role_id) + if role is not None: + try: + await member.add_roles(role) + except (discord.Forbidden, discord.HTTPException): + pass + else: + async with self.config.guild(guild).yesterdays() as yesterdays: + yesterdays.append(member.id) + channel = guild.get_channel(guild_config.get("channel")) + if channel is not None: + await channel.send(embed=embed) + + async def clean_bdays(self): + birthdays = await self.get_all_date_configs() + for guild_id, guild_bdays in birthdays.items(): + for date, bdays in guild_bdays.items(): + for user_id, year in bdays.items(): + if not any(g.get_member(int(user_id)) is not None for g in self.bot.guilds): + async with self.get_date_config(guild_id, date)() as config_bdays: + del config_bdays[user_id] + config_bdays = await self.get_date_config(guild_id, date)() + if len(config_bdays) == 0: + await self.get_date_config(guild_id, date).clear() + + async def remove_user_bday(self, guild_id: int, user_id: int): + user_id = str(user_id) + birthdays = await self.get_guild_date_configs(guild_id) + for date, user_ids in birthdays.items(): + if user_id in user_ids: + await self.get_date_config(guild_id, date).get_attr(user_id).clear() + + + async def clean_yesterday_bdays(self): + all_guild_configs = await self.config.all_guilds() + for guild_id, guild_config in all_guild_configs.items(): + for user_id in guild_config.get("yesterdays", []): + asyncio.ensure_future(self.clean_bday(guild_id, guild_config, user_id)) + await self.config.guild(discord.Guild(data={"id": guild_id}, state=None)).yesterdays.clear() + + async def do_today_bdays(self): + guild_configs = await self.get_all_date_configs() + for guild_id, guild_config in guild_configs.items(): + this_date = datetime.datetime.utcnow().date().replace(year=1) + todays_bday_config = guild_config.get(str(this_date.toordinal()), {}) + for user_id, year in todays_bday_config.items(): + asyncio.ensure_future(self.handle_bday(int(user_id), year)) + + async def maybe_update_guild(self, guild: discord.Guild): + if not guild.unavailable and guild.large: + if not guild.chunked or any(m.joined_at is None for m in guild.members): + await self.bot.request_offline_members(guild) + + def parse_date(self, date_str: str): + result = None + try: + result = datetime.datetime.strptime(date_str, "%m-%d").date().replace(year=1) + except ValueError: + pass + return result + + async def check_breaking_change(self): + await self.bot.wait_until_ready() + previous = await self.config.custom(self.DATE_GROUP).all() + if len(previous) > 0: + await self.config.custom(self.DATE_GROUP).clear() + owner = self.bot.get_user(self.bot.owner_id) + if len(self.bot.guilds) == 1: + await self.get_guild_date_config(self.bot.guilds[0].id).set_raw(value=previous) + self.logger.info("Birthdays are now per-guild. Previous birthdays have been copied.") + else: + await self.config.custom(self.GUILD_DATE_GROUP, "backup").set_raw(value=previous) + self.logger.info("Previous birthdays have been backed up in the config file.") + + + + def get_date_config(self, guild_id: int, date: int) -> Group: + return self.config.custom(self.GUILD_DATE_GROUP, str(guild_id), str(date)) + + def get_guild_date_config(self, guild_id: int) -> Group: + return self.config.custom(self.GUILD_DATE_GROUP, str(guild_id)) + + async def get_guild_date_configs(self, guild_id: int) -> dict: + return await self.get_guild_date_config(guild_id).all() + + async def get_all_date_configs(self) -> dict: + return await self.config.custom(self.GUILD_DATE_GROUP).all() diff --git a/birthday/info.json b/birthday/info.json new file mode 100644 index 0000000..7702487 --- /dev/null +++ b/birthday/info.json @@ -0,0 +1,21 @@ +{ + "author": [ + "PancakeSparkle" + + ], + "bot_version": [ + 3, + 0, + 0 + ], + "description": "Cog used for setting birthdays and announcing them and giving people roles on their birthday", + "hidden": false, + "install_msg": "Use [p]bday to bring up the help menu", + "requirements": [], + "short": "Cog used for setting birthdays and announcing them and giving people roles on their birthday", + "tags": [ + "brandons209", + "pancakesparkle", + "punish" + ] + } \ No newline at end of file