From 51a907e965234485076f96dc26f83fe8731a604a Mon Sep 17 00:00:00 2001 From: japandotorg Date: Tue, 19 Mar 2024 19:03:12 +0530 Subject: [PATCH] [BoostUtils] new cog (WIP) --- README.md | 1 + boostutils/_tagscript.py | 53 ++++++++++ boostutils/abc.py | 24 +++++ boostutils/commands/message.py | 172 +++++++++++++++++++++++++++++++++ boostutils/core.py | 57 +++++++++++ boostutils/events.py | 86 +++++++++++++++++ boostutils/info.json | 19 ++++ boostutils/utils.py | 34 +++++++ 8 files changed, 446 insertions(+) create mode 100644 boostutils/_tagscript.py create mode 100644 boostutils/abc.py create mode 100644 boostutils/commands/message.py create mode 100644 boostutils/core.py create mode 100644 boostutils/events.py create mode 100644 boostutils/info.json create mode 100644 boostutils/utils.py diff --git a/README.md b/README.md index 6c62368..aacd3c5 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ To add the cogs to your instance please do: `[p]repo add Seina-Cogs https://gith | StableDiffusion | 0.1.0 |
Stable Diffusion using the Replicate API.Stable Diffusion using the Replicate API.
| | ModManager | 0.1.0 |
Force ban/kick users.Force ban/kick users so that they stay in the ban/kick list even if someone tries to manually unban them.
| | AutoDelete | 0.1.0 |
Auto delete messages in specific channels with fancy html logging.Auto delete messages in specific channels with fancy html logging.
| +| BoostUtils | 0.1.0 |
Booster Utilities. (WIP)Various nitro boosting utilities. (WORK IN PROGRESS)
| Any questions you can find [Melon](https://discord.com/oauth2/authorize?client_id=808706062013825036&scope=bot&permissions=1099511627767%20applications.commands) and myself over on [the support server](https://discord.gg/mXfYuMy92r) diff --git a/boostutils/_tagscript.py b/boostutils/_tagscript.py new file mode 100644 index 0000000..8bba77a --- /dev/null +++ b/boostutils/_tagscript.py @@ -0,0 +1,53 @@ +from typing import Any, Dict, Final, List, final + +import TagScriptEngine as tse +from redbot.core import commands +from redbot.core.utils.chat_formatting import humanize_number + +boosted: Final[str] = "{member(mention)} just boosted the server." +unboosted: Final[str] = "{member(mention)} just unboosted the server." + +TAGSCRIPT_LIMIT: Final[int] = 10_000 + +blocks: List[tse.Block] = [ + tse.LooseVariableGetterBlock(), + tse.AssignmentBlock(), + tse.CommandBlock(), + tse.EmbedBlock(), +] + +engine: tse.Interpreter = tse.Interpreter(blocks) + + +def process_tagscript(content: str, seed_variables: Dict[str, tse.Adapter] = {}) -> Dict[str, Any]: + output: tse.Response = engine.process(content, seed_variables) + kwargs: Dict[str, Any] = {} + if output.body: + kwargs["content"] = output.body[:2000] + if embed := output.actions.get("embed"): + kwargs["embed"] = embed + return kwargs + + +class TagError(Exception): + """Base exception class.""" + + +@final +class TagCharacterLimitReached(TagError): + """Raised when the TagScript character limit is reached.""" + + def __init__(self, limit: int, length: int): + super().__init__( + f"TagScript cannot be longer than {humanize_number(limit)} (**{humanize_number(length)}**)." + ) + + +@final +class TagscriptConverter(commands.Converter[str]): + async def convert(self, ctx: commands.Context, argument: str) -> str: # type: ignore + try: + await ctx.cog.validate_tagscript(argument) # type: ignore + except TagError as e: + raise commands.BadArgument(str(e)) + return argument diff --git a/boostutils/abc.py b/boostutils/abc.py new file mode 100644 index 0000000..89c0ca6 --- /dev/null +++ b/boostutils/abc.py @@ -0,0 +1,24 @@ +from abc import ABC, ABCMeta, abstractmethod +from typing import Any + +from redbot.core.bot import Red +from redbot.core import commands, Config + + +class CompositeMetaClass(commands.CogMeta, ABCMeta): + pass + + +class MixinMeta(ABC, metaclass=CompositeMetaClass): + bot: Red + config: Config + + def __init__(self, *_args: Any) -> None: + super().__init__(*_args) + + @staticmethod + @abstractmethod + async def validate_tagscript(tagscript: str) -> bool: ... + + @abstractmethod + def format_help_for_context(self, ctx: commands.Context) -> str: ... diff --git a/boostutils/commands/message.py b/boostutils/commands/message.py new file mode 100644 index 0000000..05b39bf --- /dev/null +++ b/boostutils/commands/message.py @@ -0,0 +1,172 @@ +from typing import Dict, List, Literal, Optional, Union + +import discord +from redbot.core import commands +from redbot.core.utils.views import SimpleMenu +from redbot.core.utils.chat_formatting import box, humanize_list + +from ..abc import MixinMeta, CompositeMetaClass +from .._tagscript import TagscriptConverter +from ..utils import group_embeds_by_fields + + +class MessageCommands(MixinMeta, metaclass=CompositeMetaClass): + @commands.guild_only() + @commands.group(name="boostmessage", aliases=["boostmsg", "bmsg"]) # type: ignore + async def _message(self, _: commands.GuildContext): + """Configuration commands for boost messages.""" + + @_message.command(name="settings", aliases=["show", "showsettings", "ss"]) + async def _message_settings(self, ctx: commands.GuildContext): + """See the boost messages settings configured for your server.""" + cutoff = lambda x: x if len(x) < 1024 else x[:1021] + "..." # noqa: E731 + config: Dict[str, Union[bool, List[int], str]] = await self.config.guild( + ctx.guild + ).boost_message() + channels: List[int] = config["channels"] # type: ignore + description: str = "**Enable:** {}\n\n".format(config["toggle"]) + embed: discord.Embed = discord.Embed( + title="Boost Messages Settings for **__{}__**".format(ctx.guild.name), + description=description, + color=await ctx.embed_color(), + ) + fields: List[Dict[str, Union[str, bool]]] = [ + dict( + name="**Boost Message:**", + value=box(cutoff(config["boosted"])), + inline=False, + ), + dict( + name="**Unboost Message:**", + value=box(cutoff(config["unboosted"])), + inline=False, + ), + dict( + name="**Channels:**", + value=( + humanize_list( + [ + chan.mention + for channel in channels + if (chan := ctx.guild.get_channel(channel)) + ] + ) + if channels + else "No channels configured." + ), + inline=False, + ), + ] + embeds: List[discord.Embed] = [ + embed, + *await group_embeds_by_fields( + *fields, + per_embed=3, + title="Boost Messages Settings for **__{}__**".format(ctx.guild.name), + color=await ctx.embed_color(), + ), + ] + await SimpleMenu(embeds).start(ctx) # type: ignore + + @_message.command(name="toggle") + @commands.admin_or_permissions(manage_guild=True) + async def _message_toggle( + self, + ctx: commands.GuildContext, + true_or_false: Optional[bool] = None, + ): + """ + Enable or disable boost messages. + + - Running the command with no arguments will disable the boost messages. + """ + if true_or_false is None: + await self.config.guild(ctx.guild).boost_message.toggle.clear() # type: ignore + await ctx.send( + "Boost message is now untoggled.", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) + return + await self.config.guild(ctx.guild).boost_message.toggle.set(true_or_false) # type: ignore + await ctx.send( + f"Boost message is now {'toggle' if true_or_false else 'untoggled'}.", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) + + @_message.command(name="channels") + @commands.admin_or_permissions(manage_guild=True) + async def _message_channels( + self, + ctx: commands.GuildContext, + add_or_remove: Literal["add", "remove"], + channels: commands.Greedy[discord.TextChannel], + ): + """Add or remove the channels for boost messages.""" + async with self.config.guild(ctx.guild).boost_message.channels() as c: # type: ignore + for channel in channels: + if add_or_remove.lower() == "add": + if channel.id not in c: + c.append(channel.id) + elif add_or_remove.lower() == "remove": + if channel.id in c: + c.remove(channel.id) + await ctx.send( + f"Successfully {'added' if add_or_remove.lower() == 'add' else 'removed'} {len(channels)} channels.", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) + + @_message.group(name="message") + @commands.admin_or_permissions(manage_guild=True) + async def _message_message(self, _: commands.GuildContext): + """Configure boost and unboost messages.""" + + @_message_message.command(name="boosted", aliases=["boost"]) + async def _message_boosted( + self, ctx: commands.GuildContext, *, message: Optional[TagscriptConverter] = None + ): + """ + Configure the boost message. + + - Running the command with no arguments will reset the boost message. + """ + if message is None: + await self.config.guild(ctx.guild).boost_message.boosted.clear() # type: ignore + await ctx.send( + "Cleared the boosted message.", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) + return + await self.config.guild(ctx.guild).boost_message.boosted.set(message) # type: ignore + await ctx.send( + f"Changed the boosted message:\n{box(str(message), lang='json')}", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) + + @_message_message.command(name="unboosted", aliases=["unboost"]) + async def _message_unboosted( + self, ctx: commands.GuildContext, *, message: Optional[TagscriptConverter] = None + ): + """ + Configure the unboost message. + + - Running the command with no arguments will reset the unboost message. + """ + if message is None: + await self.config.guild(ctx.guild).boost_message.unboosted.clear() # type: ignore + await ctx.send( + "Cleared the unboosted message.", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) + return + await self.config.guild(ctx.guild).boost_message.unboosted.set(message) # type: ignore + await ctx.send( + f"Changed the unboosted message:\n{box(str(message), lang='json')}", + reference=ctx.message.to_reference(fail_if_not_exists=False), + allowed_mentions=discord.AllowedMentions(replied_user=False), + ) diff --git a/boostutils/core.py b/boostutils/core.py new file mode 100644 index 0000000..8fccf6b --- /dev/null +++ b/boostutils/core.py @@ -0,0 +1,57 @@ +from typing import Any, Dict, Final, List, Union + +from redbot.core.bot import Red +from redbot.core import commands, Config +from redbot.core.utils.chat_formatting import humanize_list + +from .abc import CompositeMetaClass +from .events import EventMixin +from .commands.message import MessageCommands +from ._tagscript import boosted, unboosted, TAGSCRIPT_LIMIT, TagCharacterLimitReached + + +class BoostUtils( + commands.Cog, + EventMixin, + MessageCommands, + metaclass=CompositeMetaClass, +): + """Nitro Boost Utilities.""" + + __author__: Final[List[str]] = ["inthedark.org"] + __version__: Final[str] = "0.1.0" + + def __init__(self, bot: Red, _args: Any) -> None: + super().__init__(*_args) + self.bot: Red = bot + self.config: Config = Config.get_conf( + self, + identifier=69_666_420, + force_registration=True, + ) + default_guild: Dict[str, Dict[str, Union[bool, List[int], str]]] = { + "boost_message": { + "toggle": False, + "channels": [], + "boosted": boosted, + "unboosted": unboosted, + } + } + self.config.register_guild(**default_guild) + + def format_help_for_context(self, ctx: commands.Context) -> str: + pre_processed: str = super().format_help_for_context(ctx) + n: str = "\n" if "\n\n" not in pre_processed else "" + text = [ + f"{pre_processed}{n}", + f"**Author:** {humanize_list(self.__author__)}", + f"**Version:** {str(self.__version__)}", + ] + return "\n".join(text) + + @staticmethod + async def validate_tagscript(tagscript: str) -> bool: + length = len(tagscript) + if length > TAGSCRIPT_LIMIT: + raise TagCharacterLimitReached(TAGSCRIPT_LIMIT, length) + return True diff --git a/boostutils/events.py b/boostutils/events.py new file mode 100644 index 0000000..9eb4ba1 --- /dev/null +++ b/boostutils/events.py @@ -0,0 +1,86 @@ +from typing import Any, Dict, List + +import discord +from redbot.core import commands + +import TagScriptEngine as tse + +from .abc import CompositeMetaClass, MixinMeta +from ._tagscript import process_tagscript, boosted, unboosted + + +class EventMixin(MixinMeta, metaclass=CompositeMetaClass): + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + if not message.guild or message.author.bot: + + return + if message.guild.system_channel_flags.premium_subscriptions and message.type in ( + discord.MessageType.premium_guild_subscription, + discord.MessageType.premium_guild_tier_1, + discord.MessageType.premium_guild_tier_2, + discord.MessageType.premium_guild_tier_3, + ): + self.bot.dispatch("member_boost", message.author) + + @commands.Cog.listener() + async def on_member_update(self, before: discord.Member, after: discord.Member) -> None: + if not after.guild: + return + if role := after.guild.premium_subscriber_role: + if role in before.roles and role not in after.roles: + self.bot.dispatch("member_unboost", before) + elif role not in before.roles and role in after.roles: + self.bot.dispatch("member_boost", after) + + @commands.Cog.listener() + async def on_member_boost(self, member: discord.Member) -> None: + guild: discord.Guild = member.guild + channels: List[int] = await self.config.guild(guild).boost_message.channels() # type: ignore + message: str = await self.config.guild(guild).boost_message.boosted() # type: ignore + kwargs: Dict[str, Any] = process_tagscript( + message, + { + "member": tse.MemberAdapter(member), + "guild": tse.GuildAdapter(guild), + }, + ) + if not kwargs: + await self.config.guild(member.guild).boost_message.boosted.clear() # type: ignore + kwargs: Dict[str, Any] = process_tagscript( + boosted, + { + "member": tse.MemberAdapter(member), + "guild": tse.GuildAdapter(guild), + }, + ) + for channel_id in channels: + channel: discord.TextChannel = guild.get_channel(channel_id) # type: ignore + if channel: + await channel.send(**kwargs) + + @commands.Cog.listener() + async def on_member_unboost(self, member: discord.Member) -> None: + guild: discord.Guild = member.guild + channels: List[int] = await self.config.guild(guild).boost_message.channels() # type: ignore + message: str = await self.config.guild(guild).boost_message.unboosted() # type: ignore + kwargs: Dict[str, Any] = process_tagscript( + message, + { + "member": tse.MemberAdapter(member), + "guild": tse.GuildAdapter(guild), + }, + ) + if not kwargs: + await self.config.guild(member.guild).boost_message.unboosted.clear() # type: ignore + kwargs: Dict[str, Any] = process_tagscript( + unboosted, + { + "member": tse.MemberAdapter(member), + "guild": tse.GuildAdapter(guild), + }, + ) + for channel_id in channels: + channel: discord.TextChannel = guild.get_channel(channel_id) # type: ignore + if channel: + await channel.send(**kwargs) diff --git a/boostutils/info.json b/boostutils/info.json new file mode 100644 index 0000000..9d86b3b --- /dev/null +++ b/boostutils/info.json @@ -0,0 +1,19 @@ +{ + "author": ["inthedark.org"], + "install_msg": "Thanks for installing the boostutils cog.", + "name": "BoostUtils", + "disabled": false, + "short": "Booster Utilities. (WIP)", + "description": "Various nitro boosting utilities. (WORK IN PROGRESS)", + "tags": [ + "boost", + "nitro", + "booster", + "nitrobooster" + ], + "required_cogs": {}, + "requirements": [ + "AdvancedTagScriptEngine==3.1.4" + ], + "type": "COG" +} diff --git a/boostutils/utils.py b/boostutils/utils.py new file mode 100644 index 0000000..258460a --- /dev/null +++ b/boostutils/utils.py @@ -0,0 +1,34 @@ +from typing import Any, Dict, List, Union + +import discord + + +@staticmethod +async def group_embeds_by_fields( + *fields: Dict[str, Union[str, bool]], + per_embed: int = 3, + page_in_footer: Union[str, bool] = True, + **kwargs: Any, +) -> List[discord.Embed]: + fix_kwargs = lambda kwargs: { # noqa: E731 + next(x): (fix_kwargs({next(x): v}) if "__" in k else v) + for k, v in kwargs.copy().items() + if (x := iter(k.split("__", 1))) + } + kwargs = fix_kwargs(kwargs) + groups: List[discord.Embed] = [] + page_format = "" + if page_in_footer: + kwargs.get("footer", {}).pop("text", None) + page_format = ( + page_in_footer if isinstance(page_in_footer, str) else "Page {page}/{total_pages}" + ) + ran = list(range(0, len(fields), per_embed)) + for ind, i in enumerate(ran): + groups.append(discord.Embed.from_dict(kwargs)) + fields_to_add = fields[i : i + per_embed] + for field in fields_to_add: + groups[ind].add_field(**field) # type: ignore + if page_format: + groups[ind].set_footer(text=page_format.format(page=ind + 1, total_pages=len(ran))) + return groups