From a799a1e9a3195b67e684070434202955bf6855c3 Mon Sep 17 00:00:00 2001 From: japandotorg Date: Mon, 5 Aug 2024 15:06:21 +0530 Subject: [PATCH] [Info] remove Mod cog as a dependency + add perms option --- info/cache.py | 2 +- info/core.py | 68 ++++++++++++------------- info/mod.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++ info/utils.py | 2 +- info/views.py | 100 +++++++++++++++++++++++------------- 5 files changed, 238 insertions(+), 72 deletions(-) create mode 100644 info/mod.py diff --git a/info/cache.py b/info/cache.py index 53dde73..941bf98 100644 --- a/info/cache.py +++ b/info/cache.py @@ -45,7 +45,7 @@ DEFAULT: Final[str] = "❔" -SelectEmoji = Literal["roles", "home", "avatar", "banner", "gavatar"] +SelectEmoji = Literal["roles", "home", "avatar", "banner", "gavatar", "perms"] StatusEmoji = Literal["online", "away", "dnd", "offline", "streaming"] DeviceEmoji = Literal[ "mobile_online", diff --git a/info/core.py b/info/core.py index 482c1d6..1e970ac 100644 --- a/info/core.py +++ b/info/core.py @@ -23,19 +23,18 @@ """ import logging +from collections.abc import Collection from typing import Dict, Final, List, Optional, Union, cast import discord -from redbot.cogs.mod.mod import Mod from redbot.core import Config, app_commands, commands from redbot.core.bot import Red -from redbot.core.errors import CogLoadError from redbot.core.utils.chat_formatting import humanize_list from .abc import CompositeMetaClass from .cache import Cache from .settings import SettingsCommands -from .utils import MELON, guild_only_and_has_embed_links +from .utils import SEINA, guild_only_and_has_embed_links from .views import CommandView, UIView log: logging.Logger = logging.getLogger("red.seina.info.core") @@ -59,8 +58,12 @@ def __init__(self, bot: Red) -> None: identifier=69_666_420, force_registration=True, ) - bot_user: discord.ClientUser = cast(discord.ClientUser, bot.user) + seina: bool = ( + self.bot.owner_ids is not None + and isinstance(self.bot.owner_ids, Collection) + and SEINA in list(self.bot.owner_ids) + ) _defaults: Dict[ str, Dict[ @@ -84,38 +87,37 @@ def __init__(self, bot: Red) -> None: "desktop_dnd": None, "desktop_offline": None, }, - "online": 859980175588589587 if bot_user.id == MELON else None, - "away": 859980300977045534 if bot_user.id == MELON else None, - "dnd": 859980375882858516 if bot_user.id == MELON else None, - "offline": 859981174007791616 if bot_user.id == MELON else None, - "streaming": 933084283197870140 if bot_user.id == MELON else None, + "online": 859980175588589587 if seina else None, + "away": 859980300977045534 if seina else None, + "dnd": 859980375882858516 if seina else None, + "offline": 859981174007791616 if seina else None, + "streaming": 933084283197870140 if seina else None, }, "badge": { - "staff": 934503593678094457 if bot_user.id == MELON else None, - "early_supporter": 934504215500423209 if bot_user.id == MELON else None, - "verified_bot_developer": 894188093647781958 if bot_user.id == MELON else None, - "active_developer": 1101083614587912293 if bot_user.id == MELON else None, - "bug_hunter": 872500602108788828 if bot_user.id == MELON else None, - "bug_hunter_level_2": 872500552481792000 if bot_user.id == MELON else None, - "partner": 934504341858029658 if bot_user.id == MELON else None, - "verified_bot": 934504704619196456 if bot_user.id == MELON else None, - "hypesquad": 934504003230892102 if bot_user.id == MELON else None, - "hypesquad_balance": 934503920552800256 if bot_user.id == MELON else None, - "hypesquad_bravery": 934503780031021076 if bot_user.id == MELON else None, - "hypesquad_brilliance": 934503837841121320 if bot_user.id == MELON else None, - "nitro": 263382460224634880 if bot_user.id == MELON else None, - "discord_certified_moderator": ( - 907486308006510673 if bot_user.id == MELON else None - ), - "bot_http_interactions": 1135879666251595817 if bot_user.id == MELON else None, + "staff": 934503593678094457 if seina else None, + "early_supporter": 934504215500423209 if seina else None, + "verified_bot_developer": 894188093647781958 if seina else None, + "active_developer": 1101083614587912293 if seina else None, + "bug_hunter": 872500602108788828 if seina else None, + "bug_hunter_level_2": 872500552481792000 if seina else None, + "partner": 934504341858029658 if seina else None, + "verified_bot": 934504704619196456 if seina else None, + "hypesquad": 934504003230892102 if seina else None, + "hypesquad_balance": 934503920552800256 if seina else None, + "hypesquad_bravery": 934503780031021076 if seina else None, + "hypesquad_brilliance": 934503837841121320 if seina else None, + "nitro": 263382460224634880 if seina else None, + "discord_certified_moderator": (907486308006510673 if seina else None), + "bot_http_interactions": 1135879666251595817 if seina else None, }, "settings": { "select": { - "roles": 1261166986050932758 if bot_user.id == MELON else None, - "home": 1047886542376538143 if bot_user.id == MELON else None, - "avatar": 934507937228017745 if bot_user.id == MELON else None, - "banner": 934508352971603998 if bot_user.id == MELON else None, - "gavatar": 1261167779957047337 if bot_user.id == MELON else None, + "roles": 1261166986050932758 if seina else None, + "home": 1047886542376538143 if seina else None, + "avatar": 934507937228017745 if seina else None, + "banner": 934508352971603998 if seina else None, + "gavatar": 1261167779957047337 if seina else None, + "perms": 934503593678094457 if seina else None, }, "downloader": False, }, @@ -195,7 +197,7 @@ async def _command_info(self, ctx: commands.Context, *, command: commands.Comman view: CommandView = CommandView(ctx, command) embeds: List[discord.Embed] = [] embeds.append(await view._make_embed()) - if (embed := await view._get_downloader_info()) and self.cache.get_downloader_info(): + if self.cache.get_downloader_info() and (embed := await view._get_downloader_info()): embeds.append(embed) _out: discord.Message = await ctx.send( embeds=embeds, @@ -213,8 +215,6 @@ async def _user_info_context( async def setup(bot: Red) -> None: - if Mod.__name__ not in bot.cogs: - raise CogLoadError("The Mod cog is required to be loaded to use this cog.") if userinfo := bot.get_command("userinfo"): global OLD_USERINFO_COMMAND OLD_USERINFO_COMMAND = cast(commands.Command, bot.remove_command(userinfo.qualified_name)) diff --git a/info/mod.py b/info/mod.py new file mode 100644 index 0000000..c400909 --- /dev/null +++ b/info/mod.py @@ -0,0 +1,138 @@ +""" +MIT License + +Copyright (c) 2024-present japandotorg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +# https://github.com/Cog-Creators/Red-DiscordBot/blob/V3/develop/redbot/cogs/mod/names.py + +from typing import List, Optional, Tuple, cast + +import discord +from redbot.core.bot import Red +from redbot.cogs.mod.mod import Mod +from redbot.core.utils.common_filters import escape_spoilers_and_mass_mentions + + +class ModUtils: + def __init__(self, bot: Red) -> None: + self.bot: Red = bot + + def handle_custom(self, user: discord.Member) -> Tuple[Optional[str], discord.ActivityType]: + a = [c for c in user.activities if c.type == discord.ActivityType.custom] + if not a: + return None, discord.ActivityType.custom + a = a[0] + c_status = None + if not a.name and not a.emoji: # type: ignore + return None, discord.ActivityType.custom + elif a.name and a.emoji: # type: ignore + c_status = "Custom: {emoji} {name}".format(emoji=a.emoji, name=a.name) # type: ignore + elif a.emoji: # type: ignore + c_status = "Custom: {emoji}".format(emoji=a.emoji) # type: ignore + elif a.name: + c_status = "Custom: {name}".format(name=a.name) + return c_status, discord.ActivityType.custom + + def handle_playing(self, user: discord.Member) -> Tuple[Optional[str], discord.ActivityType]: + p_acts = [c for c in user.activities if c.type == discord.ActivityType.playing] + if not p_acts: + return None, discord.ActivityType.playing + p_act = p_acts[0] + act = "Playing: {name}".format(name=p_act.name) + return act, discord.ActivityType.playing + + def handle_streaming(self, user: discord.Member) -> Tuple[Optional[str], discord.ActivityType]: + s_acts = [c for c in user.activities if c.type == discord.ActivityType.streaming] + if not s_acts: + return None, discord.ActivityType.streaming + s_act = s_acts[0] + if isinstance(s_act, discord.Streaming): + act = "Streaming: [{name}{sep}{game}]({url})".format( + name=discord.utils.escape_markdown(s_act.name), + sep=" | " if s_act.game else "", + game=discord.utils.escape_markdown(s_act.game) if s_act.game else "", + url=s_act.url, + ) # type: ignore + else: + act = "Streaming: {name}".format(name=s_act.name) + return act, discord.ActivityType.streaming + + def handle_listening(self, user: discord.Member) -> Tuple[Optional[str], discord.ActivityType]: + l_acts = [c for c in user.activities if c.type == discord.ActivityType.listening] + if not l_acts: + return None, discord.ActivityType.listening + l_act = l_acts[0] + if isinstance(l_act, discord.Spotify): + act = "Listening: [{title}{sep}{artist}]({url})".format( + title=discord.utils.escape_markdown(l_act.title), + sep=" | " if l_act.artist else "", + artist=discord.utils.escape_markdown(l_act.artist) if l_act.artist else "", + url=f"https://open.spotify.com/track/{l_act.track_id}", + ) + else: + act = "Listening: {title}".format(title=l_act.name) + return act, discord.ActivityType.listening + + def handle_watching(self, user: discord.Member) -> Tuple[Optional[str], discord.ActivityType]: + w_acts = [c for c in user.activities if c.type == discord.ActivityType.watching] + if not w_acts: + return None, discord.ActivityType.watching + w_act = w_acts[0] + act = "Watching: {name}".format(name=w_act.name) + return act, discord.ActivityType.watching + + def handle_competing(self, user: discord.Member) -> Tuple[Optional[str], discord.ActivityType]: + w_acts = [c for c in user.activities if c.type == discord.ActivityType.competing] + if not w_acts: + return None, discord.ActivityType.competing + w_act = w_acts[0] + act = "Competing in: {competing}".format(competing=w_act.name) + return act, discord.ActivityType.competing + + def get_status_string(self, user: discord.Member) -> str: + string = "" + for a in [ + self.handle_custom(user), + self.handle_playing(user), + self.handle_listening(user), + self.handle_streaming(user), + self.handle_watching(user), + self.handle_competing(user), + ]: + status_string, status_type = a + if status_string is None: + continue + string += f"{status_string}\n" + return string + + async def get_names( + self, user: discord.Member + ) -> Tuple[Optional[List[str]], Optional[List[str]]]: + if not (cog := cast(Optional[Mod], self.bot.get_cog("Mod"))): + return None, None + usernames: List[str] = await cog.config.user(user).past_names() + nicks: List[str] = await cog.config.member(user).past_nicks() + usernames: List[str] = list( + map(escape_spoilers_and_mass_mentions, filter(None, usernames)) + ) + nicks: List[str] = list(map(escape_spoilers_and_mass_mentions, filter(None, nicks))) + return usernames, nicks diff --git a/info/utils.py b/info/utils.py index 1bec191..57a1b9b 100644 --- a/info/utils.py +++ b/info/utils.py @@ -28,7 +28,7 @@ from redbot.core import app_commands from redbot.core.bot import Red -MELON: Final[int] = 808706062013825036 # MELON's ID (i'm lazy) +SEINA: Final[int] = 759180080328081450 # to auto add emojis to my bot(s) def guild_only_and_has_embed_links(interaction: discord.Interaction[Red]) -> bool: diff --git a/info/views.py b/info/views.py index c5918a6..7c4a833 100644 --- a/info/views.py +++ b/info/views.py @@ -29,27 +29,26 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union, cast import discord -from redbot.cogs.downloader.downloader import Downloader -from redbot.cogs.mod.mod import Mod from redbot.core import commands from redbot.core.bot import Red from redbot.core.tree import RedTree from redbot.core.utils import AsyncIter, views +from redbot.cogs.downloader.downloader import Downloader from redbot.core.utils.chat_formatting import box, humanize_list, pagify from redbot.core.utils.common_filters import filter_invites from .cache import Cache -from .utils import get_roles, truncate +from .mod import ModUtils +from .utils import get_perms, get_roles, truncate class InteractionSimpleMenu(views.SimpleMenu): async def inter(self, interaction: discord.Interaction[Red]) -> None: + await interaction.response.defer() self._fallback_author_to_ctx = False self.author: discord.abc.User = interaction.user kwargs: Dict[str, Any] = await self.get_page(self.current_page) - self.message: discord.Message = await cast( - RedTree, interaction.client.tree - )._send_from_interaction(interaction, **kwargs) + self.message: discord.Message = await interaction.followup.send(**kwargs) class ViewSourceCodeButton(discord.ui.Button["CommandView"]): @@ -121,7 +120,16 @@ def __init__( label="Roles", emoji=self.cache.get_select_emoji("roles"), value="roles", - description="View the user's roles..", + description="View the user's roles...", + ) + ) + if get_perms(self.user.guild_permissions): + options.append( + discord.SelectOption( + label="Permissions", + emoji=self.cache.get_select_emoji("perms"), + value="perms", + description="View the user's permissions...", ) ) super().__init__( @@ -149,6 +157,8 @@ def __init__( self.bot: Red = ctx.bot self.cache: Cache = cache + self.mod: ModUtils = ModUtils(self.bot) + self.user: discord.Member = user self.banner_and_gavatar: Tuple[Optional[discord.Asset], Optional[discord.Asset]] = ( banner_and_gavatar @@ -203,11 +213,54 @@ async def _callback(self: UISelect, interaction: discord.Interaction[Red]) -> No color=self.user.color, title="{}'s Roles".format(self.user.display_name) ) embed.description = ( - await self.view._format_roles() + self.view._format_roles() if get_roles(self.user) else "{} does not have any roles in this server.".format(self.user.mention) ) await interaction.edit_original_response(embed=embed) + elif value == "perms": + embed: discord.Embed = discord.Embed( + color=self.user.color, title="{}'s Permissions".format(self.user.display_name) + ) + embed.description = ( + self.view._format_perms() + if get_perms(self.user.guild_permissions) + else "{} does not have any permissions in this server.".format( + self.user.display_name + ) + ) + await interaction.edit_original_response(embed=embed) + + def _format_roles(self) -> Union[str, Any]: + roles: Optional[List[str]] = get_roles(self.user) + if roles: + string: str = ", ".join(roles) + if len(string) > 4000: + formatted: str = "and {number} more roles not displayed due to embed limits." + available_length: int = 4000 - len(formatted) + chunks = [] + remaining = 0 + for r in roles: + chunk = "{}\n".format(r) + size = len(chunk) + if size < available_length: + available_length -= size + chunks.append(chunk) + else: + remaining += 1 + chunks.append(formatted.format(number=remaining)) + string: str = "".join(chunks) + else: + string: str = discord.utils.MISSING + return string + + def _format_perms(self): + perms: List[str] = get_perms(self.user.guild_permissions) + if perms: + string = "\n".join(["- {}".format(perm) for perm in perms]) + else: + string: str = discord.utils.MISSING + return string @classmethod async def make_embed( @@ -239,29 +292,6 @@ async def interaction_check(self, interaction: discord.Interaction[Red], /) -> b return False return True - async def _format_roles(self) -> Union[str, Any]: - roles: Optional[List[str]] = get_roles(self.user) - if roles: - string: str = ", ".join(roles) - if len(string) > 4000: - formatted: str = "and {number} more roles not displayed due to embed limits." - available_length: int = 4000 - len(formatted) - chunks = [] - remaining = 0 - for r in roles: - chunk = "{}\n".format(r) - size = len(chunk) - if size < available_length: - available_length -= size - chunks.append(chunk) - else: - remaining += 1 - chunks.append(formatted.format(number=remaining)) - string: str = "".join(chunks) - else: - string: str = discord.utils.MISSING - return string - async def _make_embed(self) -> discord.Embed: if self.embed is not discord.utils.MISSING: return self.embed @@ -275,11 +305,9 @@ async def _make_embed(self) -> discord.Embed: if user in guild.members } ) - mod: Mod = self.bot.get_cog("Mod") # type: ignore - try: - names, _, nicks = await mod.get_names(user) - except AttributeError: - names, nicks = await mod.get_names_and_nicks(user) # type: ignore | specially for melon + + mod: ModUtils = self.mod + names, nicks = await mod.get_names(user) created_dt: float = ( cast(datetime.datetime, user.created_at) .replace(tzinfo=datetime.timezone.utc)