diff options
author | 2023-02-11 19:01:36 -0500 | |
---|---|---|
committer | 2023-02-11 19:01:36 -0500 | |
commit | f10325da81bc7d346e886a99c1658100822744ca (patch) | |
tree | 2662fd6fe3e4182051a3c731033eaa46a93c7a03 | |
parent | Update bot/exts/moderation/infraction/_utils.py (diff) | |
parent | Merge pull request #2396 from shtlrs/bump-bot-core-to-9-4-1 (diff) |
Merge branch 'main' into swfarnsworth-modmail-account-mention
-rw-r--r-- | bot/constants.py | 2 | ||||
-rw-r--r-- | bot/exts/help_channels/_channel.py | 30 | ||||
-rw-r--r-- | bot/exts/info/subscribe.py | 120 | ||||
-rw-r--r-- | bot/exts/moderation/modlog.py | 7 | ||||
-rw-r--r-- | bot/resources/tags/underscore.md | 27 | ||||
-rw-r--r-- | config-default.yml | 3 | ||||
-rw-r--r-- | poetry.lock | 18 | ||||
-rw-r--r-- | pyproject.toml | 2 |
8 files changed, 164 insertions, 45 deletions
diff --git a/bot/constants.py b/bot/constants.py index 9851aea97..3c29ce887 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -465,6 +465,8 @@ class Channels(metaclass=YAMLGetter): big_brother_logs: int + roles: int + class Webhooks(metaclass=YAMLGetter): section = "guild" diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index fad2a32a9..f64162006 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -14,26 +14,26 @@ from bot.log import get_logger log = get_logger(__name__) ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" - +BRANDING_REPO_RAW_URL = "https://raw.githubusercontent.com/python-discord/branding" POST_TITLE = "Python help channel" + NEW_POST_MSG = f""" **Remember to:** • **Ask** your Python question, not if you can ask or if there's an expert who can help. -• **Show** a code sample as text (rather than a screenshot) and the error message, if you got one. +• **Show** a code sample as text (rather than a screenshot) and the error message, if you've got one. • **Explain** what you expect to happen and what actually happens. For more tips, check out our guide on [asking good questions]({ASKING_GUIDE_URL}). """ -POST_FOOTER = f"Closes after a period of inactivity, or when you send {constants.Bot.prefix}close." +NEW_POST_FOOTER = f"Closes after a period of inactivity, or when you send {constants.Bot.prefix}close." +NEW_POST_ICON_URL = f"{BRANDING_REPO_RAW_URL}/main/icons/checkmark/green-checkmark-dist.png" -DORMANT_MSG = f""" -This help channel has been marked as **dormant** and locked. \ -It is no longer possible to send messages in this channel. - -If your question wasn't answered yet, you can create a new post in <#{constants.Channels.help_system_forum}>. \ -Consider rephrasing the question to maximize your chance of getting a good answer. \ -If you're not sure how, have a look through our guide for **[asking a good question]({ASKING_GUIDE_URL})**. +CLOSED_POST_MSG = f""" +This help channel has been closed and it's no longer possible to send messages here. \ +If your question wasn't answered, feel free to create a new post in <#{constants.Channels.help_system_forum}>. \ +To maximize your chances of getting a response, check out this guide on [asking good questions]({ASKING_GUIDE_URL}). """ +CLOSED_POST_ICON_URL = f"{BRANDING_REPO_RAW_URL}/main/icons/zzz/zzz-dist.png" def is_help_forum_post(channel: discord.abc.GuildChannel) -> bool: @@ -44,9 +44,11 @@ def is_help_forum_post(channel: discord.abc.GuildChannel) -> bool: async def _close_help_post(closed_post: discord.Thread, closing_reason: _stats.ClosingReason) -> None: """Close the help post and record stats.""" - embed = discord.Embed(description=DORMANT_MSG) + embed = discord.Embed(description=CLOSED_POST_MSG) + embed.set_author(name=f"{POST_TITLE} closed", icon_url=CLOSED_POST_ICON_URL) + await closed_post.send(embed=embed) - await closed_post.edit(archived=True, locked=True, reason="Locked a dormant help post") + await closed_post.edit(archived=True, locked=True, reason="Locked a closed help post") _stats.report_post_count() await _stats.report_complete_session(closed_post, closing_reason) @@ -71,8 +73,8 @@ async def send_opened_post_message(post: discord.Thread) -> None: color=constants.Colours.bright_green, description=NEW_POST_MSG, ) - embed.set_author(name=POST_TITLE) - embed.set_footer(text=POST_FOOTER) + embed.set_author(name=f"{POST_TITLE} opened", icon_url=NEW_POST_ICON_URL) + embed.set_footer(text=NEW_POST_FOOTER) await post.send(embed=embed) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 36304539f..86a209214 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -13,6 +13,7 @@ from bot import constants from bot.bot import Bot from bot.decorators import redirect_output from bot.log import get_logger +from bot.utils.channel import get_or_fetch_channel @dataclass(frozen=True) @@ -60,11 +61,25 @@ log = get_logger(__name__) class RoleButtonView(discord.ui.View): - """A list of SingleRoleButtons to show to the member.""" + """ + A view that holds the list of SingleRoleButtons to show to the member. + + Attributes + __________ + interaction_owner: discord.Member + The member that initiated the interaction + """ + + interaction_owner: discord.Member - def __init__(self, member: discord.Member): - super().__init__() + def __init__(self, member: discord.Member, assignable_roles: list[AssignableRole]): + super().__init__(timeout=DELETE_MESSAGE_AFTER) self.interaction_owner = member + author_roles = [role.id for role in member.roles] + + for index, role in enumerate(assignable_roles): + row = index // ITEMS_PER_ROW + self.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) async def interaction_check(self, interaction: Interaction) -> bool: """Ensure that the user clicking the button is the member who invoked the command.""" @@ -78,12 +93,12 @@ class RoleButtonView(discord.ui.View): class SingleRoleButton(discord.ui.Button): - """A button that adds or removes a role from the member depending on it's current state.""" + """A button that adds or removes a role from the member depending on its current state.""" ADD_STYLE = discord.ButtonStyle.success REMOVE_STYLE = discord.ButtonStyle.red UNAVAILABLE_STYLE = discord.ButtonStyle.secondary - LABEL_FORMAT = "{action} role {role_name}." + LABEL_FORMAT = "{action} role {role_name}" CUSTOM_ID_FORMAT = "subscribe-{role_id}" def __init__(self, role: AssignableRole, assigned: bool, row: int): @@ -123,7 +138,7 @@ class SingleRoleButton(discord.ui.Button): self.assigned = not self.assigned await self.update_view(interaction) - await interaction.response.send_message( + await interaction.followup.send( self.LABEL_FORMAT.format(action="Added" if self.assigned else "Removed", role_name=self.role.name), ephemeral=True, ) @@ -133,15 +148,45 @@ class SingleRoleButton(discord.ui.Button): self.style = self.REMOVE_STYLE if self.assigned else self.ADD_STYLE self.label = self.LABEL_FORMAT.format(action="Remove" if self.assigned else "Add", role_name=self.role.name) try: - await interaction.message.edit(view=self.view) + await interaction.response.edit_message(view=self.view) except discord.NotFound: log.debug("Subscribe message for %s removed before buttons could be updated", interaction.user) self.view.stop() +class AllSelfAssignableRolesView(discord.ui.View): + """A persistent view that'll hold one button allowing interactors to toggle all available self-assignable roles.""" + + def __init__(self, assignable_roles: list[AssignableRole]): + super().__init__(timeout=None) + self.assignable_roles = assignable_roles + + @discord.ui.button( + style=discord.ButtonStyle.success, + label="Show all self assignable roles", + custom_id="toggle-available-roles-button", + row=1 + ) + async def show_all_self_assignable_roles(self, interaction: Interaction, button: discord.ui.Button) -> None: + """Sends the original subscription view containing the available self assignable roles.""" + view = RoleButtonView(interaction.user, self.assignable_roles) + await interaction.response.send_message( + view=view, + ephemeral=True + ) + + class Subscribe(commands.Cog): """Cog to allow user to self-assign & remove the roles present in ASSIGNABLE_ROLES.""" + GREETING_EMOJI = ":wave:" + + SELF_ASSIGNABLE_ROLES_MESSAGE = ( + f"Hi there {GREETING_EMOJI}," + "\nWe have self-assignable roles for server updates and events!" + "\nClick the button below to toggle them:" + ) + def __init__(self, bot: Bot): self.bot = bot self.assignable_roles: list[AssignableRole] = [] @@ -150,7 +195,6 @@ class Subscribe(commands.Cog): async def cog_load(self) -> None: """Initialise the cog by resolving the role IDs in ASSIGNABLE_ROLES to role names.""" await self.bot.wait_until_guild_available() - self.guild = self.bot.get_guild(constants.Guild.id) for role in ASSIGNABLE_ROLES: @@ -170,6 +214,10 @@ class Subscribe(commands.Cog): self.assignable_roles.sort(key=operator.attrgetter("name")) self.assignable_roles.sort(key=operator.methodcaller("is_currently_available"), reverse=True) + placeholder_message_view_tuple = await self._fetch_or_create_self_assignable_roles_message() + self_assignable_roles_message, self_assignable_roles_view = placeholder_message_view_tuple + self._attach_persistent_roles_view(self_assignable_roles_message, self_assignable_roles_view) + @commands.cooldown(1, 10, commands.BucketType.member) @commands.command(name="subscribe", aliases=("unsubscribe",)) @redirect_output( @@ -178,22 +226,58 @@ class Subscribe(commands.Cog): ) async def subscribe_command(self, ctx: commands.Context, *_) -> None: # We don't actually care about the args """Display the member's current state for each role, and allow them to add/remove the roles.""" - button_view = RoleButtonView(ctx.author) - author_roles = [role.id for role in ctx.author.roles] - for index, role in enumerate(self.assignable_roles): - row = index // ITEMS_PER_ROW - button_view.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) - + view = RoleButtonView(ctx.author, self.assignable_roles) await ctx.send( "Click the buttons below to add or remove your roles!", - view=button_view, - delete_after=DELETE_MESSAGE_AFTER, + view=view, + delete_after=DELETE_MESSAGE_AFTER ) + async def _fetch_or_create_self_assignable_roles_message(self) -> tuple[discord.Message, discord.ui.View | None]: + """ + Fetches the message that holds the self assignable roles view. + + If the initial message isn't found, a new one will be created. + This message will always be needed to attach the persistent view to it + """ + roles_channel: discord.TextChannel = await get_or_fetch_channel(constants.Channels.roles) + + async for message in roles_channel.history(limit=30): + if message.content == self.SELF_ASSIGNABLE_ROLES_MESSAGE: + log.debug(f"Found self assignable roles view message: {message.id}") + return message, None + + log.debug("Self assignable roles view message hasn't been found, creating a new one.") + view = AllSelfAssignableRolesView(self.assignable_roles) + placeholder_message = await roles_channel.send(self.SELF_ASSIGNABLE_ROLES_MESSAGE, view=view) + return placeholder_message, view + + def _attach_persistent_roles_view( + self, + placeholder_message: discord.Message, + persistent_roles_view: discord.ui.View | None = None + ) -> None: + """ + Attaches the persistent view that toggles self assignable roles to its placeholder message. + + The message is searched for/created upon loading the Cog. + + Parameters + __________ + :param placeholder_message: The message that will hold the persistent view allowing + users to toggle the RoleButtonView + :param persistent_roles_view: The view attached to the placeholder_message + If none, a new view will be created + """ + if not persistent_roles_view: + persistent_roles_view = AllSelfAssignableRolesView(self.assignable_roles) + + self.bot.add_view(persistent_roles_view, message_id=placeholder_message.id) + async def setup(bot: Bot) -> None: - """Load the Subscribe cog.""" - if len(ASSIGNABLE_ROLES) > ITEMS_PER_ROW*5: # Discord limits views to 5 rows of buttons. + """Load the 'Subscribe' cog.""" + if len(ASSIGNABLE_ROLES) > ITEMS_PER_ROW * 5: # Discord limits views to 5 rows of buttons. log.error("Too many roles for 5 rows, not loading the Subscribe cog.") else: await bot.add_cog(Subscribe(bot)) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index eeaf69139..2c94d1af8 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -534,7 +534,7 @@ class ModLog(Cog, name="ModLog"): return self.is_channel_ignored(message.channel.id) - def is_channel_ignored(self, channel_id: int) -> bool: + def is_channel_ignored(self, channel: int | GuildChannel | Thread) -> bool: """ Return true if the channel, or parent channel in the case of threads, passed should be ignored by modlog. @@ -543,7 +543,8 @@ class ModLog(Cog, name="ModLog"): 2. Channels that mods do not have view permissions to 3. Channels in constants.Guild.modlog_blacklist """ - channel = self.bot.get_channel(channel_id) + if isinstance(channel, int): + channel = self.bot.get_channel(channel) # Ignore not found channels, DMs, and messages outside of the main guild. if not channel or channel.guild is None or channel.guild.id != GuildConstant.id: @@ -833,7 +834,7 @@ class ModLog(Cog, name="ModLog"): @Cog.listener() async def on_thread_delete(self, thread: Thread) -> None: """Log thread deletion.""" - if self.is_channel_ignored(thread.id): + if self.is_channel_ignored(thread): log.trace("Ignoring deletion of thread %s (%d)", thread.mention, thread.id) return diff --git a/bot/resources/tags/underscore.md b/bot/resources/tags/underscore.md new file mode 100644 index 000000000..4da2e86ca --- /dev/null +++ b/bot/resources/tags/underscore.md @@ -0,0 +1,27 @@ +--- +aliases: ["under"] +embed: + title: "Meanings of Underscores in Identifier Names" +--- + +• `__name__`: Used to implement special behaviour, such as the `+` operator for classes with the `__add__` method. [More info](https://dbader.org/blog/python-dunder-methods) +• `_name`: Indicates that a variable is "private" and should only be used by the class or module that defines it +• `name_`: Used to avoid naming conflicts. For example, as `class` is a keyword, you could call a variable `class_` instead +• `__name`: Causes the name to be "mangled" if defined inside a class. [More info](https://docs.python.org/3/tutorial/classes.html#private-variables) + +A single underscore, **`_`**, has multiple uses: +• To indicate an unused variable, e.g. in a for loop if you don't care which iteration you are on +```python +for _ in range(10): + print("Hello World") +``` +• In the REPL, where the previous result is assigned to the variable `_` +```python +>>> 1 + 1 # Evaluated and stored in `_` + 2 +>>> _ + 3 # Take the previous result and add 3 + 5 +``` +• In integer literals, e.g. `x = 1_500_000` can be written instead of `x = 1500000` to improve readability + +See also ["Reserved classes of identifiers"](https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers) in the Python docs, and [this more detailed guide](https://dbader.org/blog/meaning-of-underscores-in-python). diff --git a/config-default.yml b/config-default.yml index 1d7a2ff78..ac76a670a 100644 --- a/config-default.yml +++ b/config-default.yml @@ -235,6 +235,9 @@ guild: # Watch big_brother_logs: &BB_LOGS 468507907357409333 + # Information + roles: 851270062434156586 + moderation_categories: - *MODS_CATEGORY - *MODMAIL diff --git a/poetry.lock b/poetry.lock index f418c0516..b8c30b5b3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -193,7 +193,7 @@ cli = ["clevercsv (==0.7.4)", "click (==8.1.3)", "pyyaml (==6.0)", "toml (==0.10 [[package]] name = "discord-py" -version = "2.1.0" +version = "2.1.1" description = "A Python wrapper for the Discord API" category = "main" optional = false @@ -685,16 +685,16 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pydis-core" -version = "9.1.1" +version = "9.4.1" description = "PyDis core provides core functionality and utility to the bots of the Python Discord community." category = "main" optional = false -python-versions = ">=3.10.0,<3.11.0" +python-versions = ">=3.10.0,<3.12.0" [package.dependencies] aiodns = "3.0.0" async-rediscache = {version = "1.0.0rc2", extras = ["fakeredis"], optional = true, markers = "extra == \"async-rediscache\""} -"discord.py" = "2.1.0" +"discord.py" = "2.1.1" statsd = "4.0.1" [package.extras] @@ -1113,7 +1113,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.10.*" -content-hash = "8c741bfe48fa990572979642dbb94c170385a15793c234044a08cde3dce629e1" +content-hash = "378b9f9a41e79cdd30ee594a9017f709a8286b7dfbfe45215f60ca7bad0f53fd" [metadata.files] aiodns = [ @@ -1376,8 +1376,8 @@ deepdiff = [ {file = "deepdiff-6.2.1.tar.gz", hash = "sha256:3fe134dde5b3922ff8c51fc1e95a972e659c853797231b836a5ccf15532fd516"}, ] discord-py = [ - {file = "discord.py-2.1.0-py3-none-any.whl", hash = "sha256:a2cfa9f09e3013aaaa43600cc8dfaf67c532dd34afcb71e550f5a0dc9133a5e0"}, - {file = "discord.py-2.1.0.tar.gz", hash = "sha256:027ccdd22b5bb66a9e19cbd8daa1bc74b49271a16a074d57e52f288fcfa208e8"}, + {file = "discord.py-2.1.1-py3-none-any.whl", hash = "sha256:b99fbf4ad74f007c680f33f5174738858146c99a2bc4f6d0e08ddbf93bc12c4d"}, + {file = "discord.py-2.1.1.tar.gz", hash = "sha256:8258b7af641c532e9e33e186c7b867d9ff5575fe63a460b17ed55bd4fe082200"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, @@ -1909,8 +1909,8 @@ pydantic = [ {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] pydis-core = [ - {file = "pydis_core-9.1.1-py3-none-any.whl", hash = "sha256:db6e29c8eb4c12ce29fbf57f7b7214a1bdf702f9464320f4c39fef4dc8741bc0"}, - {file = "pydis_core-9.1.1.tar.gz", hash = "sha256:a73fe400d9b7cea5d87cfb53d1bb9ca8fd7fd3b48c1c1b74c43117319acefdac"}, + {file = "pydis_core-9.4.1-py3-none-any.whl", hash = "sha256:29f1f69ae2bc5c9560ac3d43149ca9e7f13d567064555b76b79be5c8fa9ba6ba"}, + {file = "pydis_core-9.4.1.tar.gz", hash = "sha256:b52c7ea052196aae72298ef59f59514376178e713cd133e0ca6fcb1d7312024c"}, ] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, diff --git a/pyproject.toml b/pyproject.toml index ac6d3982d..8a94f9ffa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" python = "3.10.*" # See https://bot-core.pythondiscord.com/ for docs. -pydis_core = { version = "9.1.1", extras = ["async-rediscache"] } +pydis_core = { version = "9.4.1", extras = ["async-rediscache"] } redis = "4.3.5" fakeredis = { version = "2.0.0", extras = ["lua"] } |