async def rename_poms(ctx: Context, old: str, new: str, session_type: SessionType) -> int: """Rename a user's poms. @param ctx The context passed by discord. @param old Current description in the session. @param new New description in the session. @param session_type The type of session to affect. @return Number of rows changed. """ session_poms_only = session_type == SessionType.CURRENT banked_poms_only = session_type == SessionType.BANKED try: changed = await Storage.update_user_poms_descriptions( ctx.author, old, new, session_poms_only=session_poms_only, banked_poms_only=banked_poms_only, ) except DataError as exc: if "Data too long" not in exc.args[1]: raise await ctx.author.send( normalize_and_dedent(f""" New description is too long: "{new}" ({len(new)} of {Config.DESCRIPTION_LIMIT} character maximum). """)) await ctx.message.add_reaction(Reactions.ROBOT) return 0 if not changed: session_name = "current session" if session_poms_only else "bank" await ctx.author.send( normalize_and_dedent(f""" No poms found matching "{old}" in your {session_name}. """)) await ctx.message.add_reaction(Reactions.ROBOT) return 0 await ctx.message.add_reaction(Reactions.CHECKMARK) return changed
async def do_bank(ctx: Context, *args): """Bank your poms. Your "Bank" is your archive of poms after you've ended a session. This command will move the poms in your current session to your bank; You can see your bank with !poms. Renaming poms: Use !bank.rename to redesignate all poms from one description to another. !bank.rename readign reading This command takes exactly two arguments; if descriptions have spaces, then you'll need to enclose them in double quotes, e.g.: !bank.rename "sturdy 4 teh fennel" "study for the final" Resetting poms: Use !bank.reset to reset your bank by deleting all your poms. CAUTION: Neither renaming nor resetting can be undone! """ if ctx.invoked_with in Config.RENAME_POMS_IN_BANK: try: old, new = args except ValueError: await ctx.author.send( normalize_and_dedent(f""" Please specify exactly two descriptions (the old one followed by the new one) inside of double quotes. See `!help bank`. """)) await ctx.message.add_reaction(Reactions.ROBOT) return await rename_poms(ctx, old, new, SessionType.BANKED) return if ctx.invoked_with in Config.RESET_POMS_IN_BANK: await Storage.delete_poms(user=ctx.author, session=SessionType.BANKED) await ctx.message.add_reaction(Reactions.WASTEBASKET) return reply_with_embed = partial(send_embed_message, ctx=None, _func=ctx.reply) if num_poms_banked := await Storage.bank_user_session_poms(ctx.author): await reply_with_embed( title="Poms Banked", description="You have successfully banked {n} pom{s}!".format( n=num_poms_banked, s="s" if num_poms_banked != 1 else "", )) await ctx.message.add_reaction(Reactions.BANK)
async def do_poms(ctx: Context, *args): """See the poms in your bank and current session. Receive a DM of your banked poms and the poms in your current session in separate lists, organized by the number of poms of any particular description. You can also share the details of your current session by typing: "!poms.show" (without quotes). To see only poms of a certain description, specify the description: !poms reading Or show them publicly: !poms.show reading Renaming poms: Use !poms.rename to redesignate all poms from one description to another. !poms.rename readign reading This command takes exactly two arguments; if descriptions have spaces, then you'll need to enclose them in double quotes, e.g.: !poms.rename "sturdy 4 teh fennel" "study for the final" Resetting poms: Use !poms.reset to reset your current session by deleting all your poms. CAUTION: Neither renaming nor resetting can be undone! """ if ctx.invoked_with in Config.RENAME_POMS_IN_SESSION: try: old, new = args except ValueError: await ctx.author.send(normalize_and_dedent("""\ Please specify exactly two descriptions (the old one followed by the new one) inside of double quotes. See `!help poms`. """)) await ctx.message.add_reaction(Reactions.ROBOT) await rename_poms(ctx, old, new, SessionType.CURRENT) return if ctx.invoked_with in Config.RESET_POMS_IN_SESSION: await Storage.delete_poms(user=ctx.author, session=SessionType.CURRENT) await ctx.message.add_reaction(Reactions.WASTEBASKET) return description = " ".join(args) poms = await Storage.get_poms(user=ctx.author, descript=description) response_is_public = ctx.invoked_with in Config.PUBLIC_POMS_ALIASES session = partial(_Session, description=description, public_response=response_is_public) banked_session = session( session_type=SessionType.BANKED, poms=[p for p in poms if not p.is_current_session()], ) current_session = session( session_type=SessionType.CURRENT, poms=[p for p in poms if p.is_current_session()], ) if response_is_public: try: await send_embed_message( None, title=f"Pom statistics for {ctx.author.display_name}", description=current_session.get_session_started_message(), thumbnail=ctx.author.avatar_url, fields=[current_session.get_message_field()], footer=current_session.get_duration_message(), _func=ctx.message.reply) except HTTPException: await ctx.author.send(normalize_and_dedent("""\ The combined length of the pom descriptions in your current session are over Discord's embed character limit. Use !poms (without `.show`) to see them and rename a few. """)) await ctx.message.add_reaction(Reactions.ROBOT) return if description: footer = "Total time spent on {description}: {duration}".format( description=description, duration=_dynamic_duration(len(poms) * Config.POM_LENGTH)) else: footer = "\n".join([ "Total time spent pomming: {}".format( _dynamic_duration( len(banked_session + current_session) * Config.POM_LENGTH)), current_session.get_duration_message(), ]) try: await send_embed_message( None, title=f"Your pom statistics", description=current_session.get_session_started_message(), thumbnail=ctx.author.avatar_url, fields=[ banked_session.get_message_field(), SPACER, current_session.get_message_field(), ], footer=footer, _func=(ctx.send if Debug.POMS_COMMAND_IS_PUBLIC else ctx.author.send), ) except HTTPException: for session in (current_session, banked_session): field = session.get_message_field() if len(field.value) <= Limits.MAX_EMBED_FIELD_VALUE: await send_embed_message( None, title="Your pom statistics", description=current_session.get_session_started_message(), thumbnail=ctx.author.avatar_url, fields=[session.get_message_field()], footer=footer, _func=ctx.author.send if not Debug.POMS_COMMAND_IS_PUBLIC else ctx.send, ) continue message = normalize_and_dedent(""" ```fix The combined length of all the pom descriptions in your {session_type} is longer than the maximum embed message field size for Discord embeds ({length}, Max is {max_length}). Please rename a few with !poms.rename (see !help {cmd}).``` """.format( session_type=session.type.value.lower(), length=len(session.get_message_field().value), max_length=Limits.MAX_EMBED_FIELD_VALUE, cmd="poms" if session.type == SessionType.CURRENT else "bank", )) await (ctx.author.send(message) if not Debug.POMS_COMMAND_IS_PUBLIC else ctx.send(message)) for message in session.iter_message_field( max_length=Limits.MAX_CHARACTERS_PER_MESSAGE - 100): await (ctx.author.send(message) if not Debug.POMS_COMMAND_IS_PUBLIC else ctx.send(message)) await ctx.message.add_reaction(Reactions.ROBOT) else: await ctx.message.add_reaction(Reactions.CHECKMARK)
class AsyncCheckRaiseResponse(unittest.mock.AsyncMock): """Mock what would be Discord API errors as Python exceptions in the same way that Disocrd.py will at runtime. See: https://canary.discord.com/developers/docs/resources/channel#embed-limits """ async def __await__(self, *args, **kwargs): """Become awaitable.""" # `__call__` is only used in Discord mocking even though everything is # awaited, so we need to ignore Pylint warnings and make the rest of the # mock object awaitable. async def __call__(self, *args, **kwargs): # pylint: disable=invalid-overridden-method """Inspect the passed arguements and raise HTTPException where the Discord API would respond with HTTP 400 in the wild. """ # Call super() first because it sets call counts and called args. We # still want to investigate that stuff even if we raise. super().__call__(*args, **kwargs) if args and sum(len(a) for a in args) > Limits.MAX_CHARACTERS_PER_MESSAGE: self.raise_bad_request("args") if not (embed := kwargs.get("embed")): return total_embed_length = 0 for attr, max_attr_length in ( ("title", Limits.MAX_EMBED_TITLE), ("description", Limits.MAX_EMBED_DESCRIPTION), ("fields", Limits.MAX_NUM_EMBED_FIELDS), ("footer", Limits.MAX_EMBED_FOOTER_TEXT), ): try: attr_length = len(getattr(embed, attr, None)) except TypeError: continue if attr_length > max_attr_length: self.raise_bad_request(f"embed {attr}") total_embed_length += attr_length # When `fields` is not specified, it's an empty list. for index, field in enumerate(embed.fields): for attr, max_attr_length in ( ("name", Limits.MAX_EMBED_FIELD_NAME), ("value", Limits.MAX_EMBED_FIELD_VALUE), ): try: attr_length = len(getattr(field, attr, None)) except TypeError: continue if attr_length > max_attr_length: await self.raise_bad_request( normalize_and_dedent(f"""\ Attribute too big: embed.fields[{index}].{attr}: {attr_length} (MAX {max_attr_length}) """)) total_embed_length += attr_length if author_name := getattr(embed.author, "name", None): if len(author_name) > Limits.MAX_EMBED_AUTHOR_NAME: self.raise_bad_request("embed author name") total_embed_length += len(author_name)
def generate_message_too_long_responses( ctx: Context, footer: str, sessions: Tuple, ) -> List[_Response]: """Generate an ordered list of messages to be sent instead of a single embed when the !poms embed is too long. The "current session" is determined automatically. @param ctx The context originally sent to the callback. @param footer The footer to be included in the new embed(s). @param session Tuple of _Session objects representing the embed fields that were too long. @return Ordered list of _Response objects. """ responses: List[_Response] = [] current_session = next(s for s in sessions if s.type == SessionType.CURRENT) normal_response = lambda msg: _Response( is_embed_message=False, args=(msg, ), _func=(ctx.author.send if not Debug.POMS_COMMAND_IS_PUBLIC else ctx.send)) for session in sessions: field = session.get_message_field() if len(field.value) <= Limits.MAX_EMBED_FIELD_VALUE: responses.append( _Response(is_embed_message=True, args=(None, ), kwargs={ "title": "Your pom statistics", "description": current_session.get_session_started_message(), "thumbnail": ctx.author.avatar_url, "fields": [session.get_message_field()], "footer": footer, "_func": (ctx.author.send if not Debug.POMS_COMMAND_IS_PUBLIC else ctx.send), })) continue message = normalize_and_dedent(""" ```fix The combined length of all the pom descriptions in your {session_type} is longer than the maximum embed message field size for Discord embeds ({length}, Max is {max_length}). Please rename a few with !{cmd}.rename (see !help {cmd}).``` """.format( session_type=session.type.value.lower(), length=len(session.get_message_field().value), max_length=Limits.MAX_EMBED_FIELD_VALUE, cmd="poms" if session.type == SessionType.CURRENT else "bank", )) responses.append(normal_response(message)) for message in session.iter_message_field( max_length=Limits.MAX_CHARACTERS_PER_MESSAGE - 100): responses.append(normal_response(message)) return sorted(responses, key=lambda r: not r.is_embed_message)
async def do_poms(ctx: Context, *args): """See the poms in your bank and current session. Receive a DM of your banked poms and the poms in your current session in separate lists, organized by the number of poms of any particular description. You can also share the details of your current session by typing: "!poms.show" (without quotes). To see only poms of a certain description, specify the description: !poms reading Or show them publicly: !poms.show reading Renaming poms: Use !poms.rename to redesignate all poms from one description to another. !poms.rename readign reading This command takes exactly two arguments; if descriptions have spaces, then you'll need to enclose them in double quotes, e.g.: !poms.rename "sturdy 4 teh fennel" "study for the final" Resetting poms: Use !poms.reset to reset your current session by deleting all your poms. CAUTION: Neither renaming nor resetting can be undone! """ if ctx.invoked_with in Config.RENAME_POMS_IN_SESSION: try: old, new = args except ValueError: await ctx.author.send( normalize_and_dedent("""\ Please specify exactly two descriptions (the old one followed by the new one) inside of double quotes. See `!help poms`. """)) await ctx.message.add_reaction(Reactions.ROBOT) await rename_poms(ctx, old, new, SessionType.CURRENT) return if ctx.invoked_with in Config.RESET_POMS_IN_SESSION: await Storage.delete_poms(user=ctx.author, session=SessionType.CURRENT) await ctx.message.add_reaction(Reactions.WASTEBASKET) return description = " ".join(args) poms = await Storage.get_poms(user=ctx.author, descript=description) response_is_public = ctx.invoked_with in Config.PUBLIC_POMS_ALIASES session = partial(_Session, description=description, public_response=response_is_public) banked_session = session( session_type=SessionType.BANKED, poms=[p for p in poms if not p.is_current_session()], ) current_session = session( session_type=SessionType.CURRENT, poms=[p for p in poms if p.is_current_session()], ) if response_is_public: await send_embed_message( None, title=f"Pom statistics for {ctx.author.display_name}", description=current_session.get_session_started_message(), thumbnail=ctx.author.avatar_url, fields=[current_session.get_message_field()], footer=current_session.get_duration_message(), _func=ctx.message.reply) return if description: footer = "Total time spent on {description}: {duration}".format( description=description, duration=_dynamic_duration(len(poms) * Config.POM_LENGTH)) else: footer = "\n".join([ "Total time spent pomming: {}".format( _dynamic_duration( len(banked_session + current_session) * Config.POM_LENGTH)), current_session.get_duration_message(), ]) await send_embed_message( None, title=f"Your pom statistics", description=current_session.get_session_started_message(), thumbnail=ctx.author.avatar_url, fields=[ banked_session.get_message_field(), SPACER, current_session.get_message_field(), ], footer=footer, _func=(ctx.send if Debug.POMS_COMMAND_IS_PUBLIC else ctx.author.send), ) await ctx.message.add_reaction(Reactions.CHECKMARK)