aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Steele Farnsworth <[email protected]>2023-02-11 19:01:36 -0500
committerGravatar GitHub <[email protected]>2023-02-11 19:01:36 -0500
commitf10325da81bc7d346e886a99c1658100822744ca (patch)
tree2662fd6fe3e4182051a3c731033eaa46a93c7a03
parentUpdate bot/exts/moderation/infraction/_utils.py (diff)
parentMerge 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.py2
-rw-r--r--bot/exts/help_channels/_channel.py30
-rw-r--r--bot/exts/info/subscribe.py120
-rw-r--r--bot/exts/moderation/modlog.py7
-rw-r--r--bot/resources/tags/underscore.md27
-rw-r--r--config-default.yml3
-rw-r--r--poetry.lock18
-rw-r--r--pyproject.toml2
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"] }