From 7846914b99910bdaac144a57322fcda25db76ea4 Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 18 Nov 2022 08:44:32 +0200 Subject: [PATCH] rewrite: New lib utils. New `MessageArgs` util. Rewrite `prop_tabulate` into `tabulate`. New `EmbedField` util. New `parse_ids` util. New `DotDict` util class. --- bot/utils/lib.py | 228 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 210 insertions(+), 18 deletions(-) diff --git a/bot/utils/lib.py b/bot/utils/lib.py index 92a832f6..a8711559 100644 --- a/bot/utils/lib.py +++ b/bot/utils/lib.py @@ -1,9 +1,14 @@ +from typing import NamedTuple, Optional, Sequence, Union, overload, List import datetime import iso8601 # type: ignore import re from contextvars import Context import discord +from discord import Embed, File, GuildSticker, StickerItem, AllowedMentions, Message, MessageReference, PartialMessage +from discord.ui import View + +from meta.errors import UserInputError # from cmdClient.lib import SafeCancellation @@ -16,32 +21,178 @@ tick = '✅' cross = '❌' -def prop_tabulate(prop_list: list[str], value_list: list[str], indent=True, colon=True) -> str: +class MessageArgs: """ - Turns a list of properties and corresponding list of values into + Utility class for storing message creation and editing arguments. + """ + # TODO: Overrides for mutually exclusive arguments, see Messageable.send + + @overload + def __init__( + self, + content: Optional[str] = ..., + *, + tts: bool = ..., + embed: Embed = ..., + file: File = ..., + stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: View = ..., + suppress_embeds: bool = ..., + ) -> None: + ... + + @overload + def __init__( + self, + content: Optional[str] = ..., + *, + tts: bool = ..., + embed: Embed = ..., + files: Sequence[File] = ..., + stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: View = ..., + suppress_embeds: bool = ..., + ) -> None: + ... + + @overload + def __init__( + self, + content: Optional[str] = ..., + *, + tts: bool = ..., + embeds: Sequence[Embed] = ..., + file: File = ..., + stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: View = ..., + suppress_embeds: bool = ..., + ) -> None: + ... + + @overload + def __init__( + self, + content: Optional[str] = ..., + *, + tts: bool = ..., + embeds: Sequence[Embed] = ..., + files: Sequence[File] = ..., + stickers: Sequence[Union[GuildSticker, StickerItem]] = ..., + delete_after: float = ..., + nonce: Union[str, int] = ..., + allowed_mentions: AllowedMentions = ..., + reference: Union[Message, MessageReference, PartialMessage] = ..., + mention_author: bool = ..., + view: View = ..., + suppress_embeds: bool = ..., + ) -> None: + ... + + def __init__(self, **kwargs): + self.kwargs = kwargs + + @property + def send_args(self) -> dict: + return self.kwargs + + @property + def edit_args(self) -> dict: + args = {} + kept = ( + 'content', 'embed', 'embeds', 'delete_after', 'allowed_mentions', 'view' + ) + for k in kept: + if k in self.kwargs: + args[k] = self.kwargs[k] + + if 'file' in self.kwargs: + args['attachments'] = [self.kwargs['file']] + + if 'files' in self.kwargs: + args['attachments'] = self.kwargs['files'] + + if 'suppress_embeds' in self.kwargs: + args['suppress'] = self.kwargs['suppress_embeds'] + + return args + + +def tabulate( + *fields: tuple[str, str], + row_format: str = "`{invis}{key:<{pad}}{colon}`\t{value}", + sub_format: str = "`{invis:<{pad}}{invis}`\t{value}", + colon: str = ':', + invis: str = "​", + **args +) -> list[str]: + """ + Turns a list of (property, value) pairs into a pretty string with one `prop: value` pair each line, padded so that the colons in each line are lined up. - Handles empty props by using an extra couple of spaces instead of a `:`. + Use `\\r\\n` in a value to break the line with padding. Parameters ---------- - prop_list: List[str] - List of short names to put on the right side of the list. - Empty props are considered to be "newlines" for the corresponding value. - value_list: List[str] - List of values corresponding to the properties above. - indent: bool - Whether to add padding so the properties are right-adjusted. + fields: List[tuple[str, str]] + List of (key, value) pairs. + row_format: str + The format string used to format each row. + sub_format: str + The format string used to format each subline in a row. + colon: str + The colon character used. + invis: str + The invisible character used (to avoid Discord stripping the string). - Returns: str + Returns: List[str] + The list of resulting table rows. + Each row corresponds to one (key, value) pair from fields. """ - max_len = max(len(prop) for prop in prop_list) - return "".join(["`{}{}{}`\t{}{}".format("​ " * (max_len - len(prop)) if indent else "", - prop, - (":" if len(prop) else "​ " * 2) if colon else '', - value_list[i], - '' if str(value_list[i]).endswith("```") else '\n') - for i, prop in enumerate(prop_list)]) + max_len = max(len(field[0]) for field in fields) + + rows = [] + for field in fields: + key = field[0] + value = field[1] + lines = value.split('\r\n') + + row_line = row_format.format( + invis=invis, + key=key, + pad=max_len, + colon=colon, + value=lines[0], + field=field, + **args + ) + if len(lines) > 1: + row_lines = [row_line] + for line in lines[1:]: + sub_line = sub_format.format( + invis=invis, + pad=max_len + len(colon), + value=line, + **args + ) + row_lines.append(sub_line) + row_line = '\n'.join(row_lines) + rows.append(row_line) + return rows def paginate_list(item_list: list[str], block_length=20, style="markdown", title=None) -> list[str]: @@ -382,6 +533,12 @@ async def mail(client: discord.Client, channelid: int, **msg_args) -> discord.Me return await channel.send(**msg_args) +class EmbedField(NamedTuple): + name: str + value: str + inline: Optional[bool] = True + + def emb_add_fields(embed: discord.Embed, emb_fields: list[tuple[str, str, bool]]): """ Append embed fields to an embed. @@ -461,3 +618,38 @@ def multiple_replace(string: str, rep_dict: dict[str, str]) -> str: def recover_context(context: Context): for var in context: var.set(context[var]) + + +def parse_ids(idstr: str) -> List[int]: + """ + Parse a provided comma separated string of maybe-mentions, maybe-ids, into a list of integer ids. + + Object agnostic, so all mention tokens are stripped. + Raises UserInputError if an id is invalid, + setting `orig` and `item` info fields. + """ + # Extract ids from string + splititer = (split.strip('<@!#&>, ') for split in idstr.split(',')) + splits = [split for split in splititer if split] + + # Check they are integers + if (not_id := next((split for split in splits if not split.isdigit()), None)) is not None: + raise UserInputError("Could not extract an id from `$item`!", {'orig': idstr, 'item': not_id}) + + # Cast to integer and return + return list(map(int, splits)) + + +def error_embed(error, **kwargs) -> discord.Embed: + embed = discord.Embed( + colour=discord.Colour.red(), + description=error, + timestamp=utc_now() + ) + return embed + + +class DotDict(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__dict__ = self