diff options
Diffstat (limited to 'bot/exts/holidays/valentines/lovecalculator.py')
| -rw-r--r-- | bot/exts/holidays/valentines/lovecalculator.py | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py new file mode 100644 index 00000000..3999db2b --- /dev/null +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -0,0 +1,99 @@ +import bisect +import hashlib +import json +import logging +import random +from pathlib import Path +from typing import Coroutine, Optional + +import discord +from discord import Member +from discord.ext import commands +from discord.ext.commands import BadArgument, Cog, clean_content + +from bot.bot import Bot +from bot.constants import Channels, Client, Lovefest, Month +from bot.utils.decorators import in_month + +log = logging.getLogger(__name__) + +LOVE_DATA = json.loads(Path("bot/resources/holidays/valentines/love_matches.json").read_text("utf8")) +LOVE_DATA = sorted((int(key), value) for key, value in LOVE_DATA.items()) + + +class LoveCalculator(Cog): + """A cog for calculating the love between two people.""" + + @in_month(Month.FEBRUARY) + @commands.command(aliases=("love_calculator", "love_calc")) + @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) + async def love(self, ctx: commands.Context, who: Member, whom: Optional[Member] = None) -> None: + """ + Tells you how much the two love each other. + + This command requires at least one member as input, if two are given love will be calculated between + those two users, if only one is given, the second member is asusmed to be the invoker. + Members are converted from: + - User ID + - Mention + - name#discrim + - name + - nickname + + Any two arguments will always yield the same result, regardless of the order of arguments: + Running .love @joe#6000 @chrisjl#2655 will always yield the same result. + Running .love @chrisjl#2655 @joe#6000 will yield the same result as before. + """ + if ( + Lovefest.role_id not in [role.id for role in who.roles] + or (whom is not None and Lovefest.role_id not in [role.id for role in whom.roles]) + ): + raise BadArgument( + "This command can only be ran against members with the lovefest role! " + "This role be can assigned by running " + f"`{Client.prefix}lovefest sub` in <#{Channels.community_bot_commands}>." + ) + + if whom is None: + whom = ctx.author + + def normalize(arg: Member) -> Coroutine: + # This has to be done manually to be applied to usernames + return clean_content(escape_markdown=True).convert(ctx, str(arg)) + + # Sort to ensure same result for same input, regardless of order + who, whom = sorted([await normalize(arg) for arg in (who, whom)]) + + # Hash inputs to guarantee consistent results (hashing algorithm choice arbitrary) + # + # hashlib is used over the builtin hash() to guarantee same result over multiple runtimes + m = hashlib.sha256(who.encode() + whom.encode()) + # Mod 101 for [0, 100] + love_percent = sum(m.digest()) % 101 + + # We need the -1 due to how bisect returns the point + # see the documentation for further detail + # https://docs.python.org/3/library/bisect.html#bisect.bisect + index = bisect.bisect(LOVE_DATA, (love_percent,)) - 1 + # We already have the nearest "fit" love level + # We only need the dict, so we can ditch the first element + _, data = LOVE_DATA[index] + + status = random.choice(data["titles"]) + embed = discord.Embed( + title=status, + description=f"{who} \N{HEAVY BLACK HEART} {whom} scored {love_percent}%!\n\u200b", + color=discord.Color.dark_magenta() + ) + embed.add_field( + name="A letter from Dr. Love:", + value=data["text"] + ) + embed.set_footer(text=f"You can unsubscribe from lovefest by using {Client.prefix}lovefest unsub") + + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Load the Love calculator Cog.""" + bot.add_cog(LoveCalculator()) |