Skip to content
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
ecaf49c
initial work
MattyTheHacker Dec 19, 2024
cc9c12a
bump deps
MattyTheHacker Dec 19, 2024
19976d0
kinda works
MattyTheHacker Dec 19, 2024
360390d
extract functionality to method
MattyTheHacker Dec 20, 2024
28ea9e3
Finalise
MattyTheHacker Dec 20, 2024
b6b6f94
yeet unreachable
MattyTheHacker Dec 20, 2024
f504402
roll back lock file
MattyTheHacker Dec 20, 2024
2eda160
fix ruff warning
MattyTheHacker Dec 20, 2024
a43795c
Add extra try catch just in case.
MattyTheHacker Dec 22, 2024
1cfea65
Merge branch 'main' into threads-with-friends
MattyTheHacker Dec 22, 2024
2c5cee3
Merge branch 'main' into threads-with-friends
MattyTheHacker Jan 1, 2025
ac11a25
Merge branch 'main' into threads-with-friends
MattyTheHacker Jan 1, 2025
219ad1d
Merge branch 'main' into threads-with-friends
MattyTheHacker Jan 2, 2025
134a43d
fix lock file
MattyTheHacker Jan 2, 2025
5d315fc
refactor to make 2 separate commands
MattyTheHacker Jan 2, 2025
6c137f4
add committee lock
MattyTheHacker Jan 2, 2025
5d585e6
Merge branch 'main' into threads-with-friends
MattyTheHacker Jan 2, 2025
7658c9a
Merge branch 'main' into threads-with-friends
MattyTheHacker Jan 2, 2025
5cdfcdc
on todays episode of scope creep !
MattyTheHacker Jan 6, 2025
8eb19fd
Bump python project action version
MattyTheHacker Jan 6, 2025
f6796a5
Update tests.yaml
MattyTheHacker Jan 6, 2025
ce291f9
Merge branch 'bump-action-version' into threads-with-friends
MattyTheHacker Jan 6, 2025
1c8d21b
update deps
MattyTheHacker Jan 23, 2025
fe9c31f
Merge branch 'main' into threads-with-friends
MattyTheHacker Jan 23, 2025
386282f
lock
MattyTheHacker Jan 23, 2025
3a87d23
Merge branch 'main' into threads-with-friends
MattyTheHacker Jan 27, 2025
fc5db72
lock
MattyTheHacker Jan 27, 2025
81347d3
Implement check for @ symbol
MattyTheHacker Mar 1, 2025
2bf3294
Merge branch 'main' into threads-with-friends
MattyTheHacker Mar 1, 2025
d88b5f2
Add config option to disable adding committee to threads
MattyTheHacker Mar 1, 2025
8481b1f
Merge branch 'main' into threads-with-friends
MattyTheHacker Mar 1, 2025
c047597
Merge branch 'main' into threads-with-friends
MattyTheHacker Mar 2, 2025
8dda974
Rename file
MattyTheHacker Mar 2, 2025
e8d47e7
Merge branch 'main' into threads-with-friends
MattyTheHacker Mar 4, 2025
f253b8b
suck my cunt pre commit f*ck off
MattyTheHacker Mar 4, 2025
4cbaa2e
fix
MattyTheHacker Mar 4, 2025
b559f0f
Fix mypy errors AGAIN BECAUSE THE PRE-COMMIT FUCKED IT
MattyTheHacker Mar 4, 2025
a8546e7
Refactor the separate commands for role and user to one
MattyTheHacker Mar 4, 2025
38b88e2
Fix new lines
MattyTheHacker Mar 4, 2025
dcedbf3
Ruff format
MattyTheHacker Mar 4, 2025
933e462
Remove doc string from private method
MattyTheHacker Mar 4, 2025
c5785af
Merge branch 'main' into threads-with-friends
MattyTheHacker Mar 5, 2025
df53007
Implement multiple roles and users adding at once
MattyTheHacker Mar 5, 2025
c5aa18d
Merge branch 'threads-with-friends' of github.com:CSSUoB/TeX-Bot-Py-V…
MattyTheHacker Mar 5, 2025
76a0729
Fix parameter names
MattyTheHacker Mar 5, 2025
54cab7d
Fix bad logic
MattyTheHacker Mar 5, 2025
fabc7a8
Update committee role syntax
MattyTheHacker Mar 6, 2025
6b3d090
Merge branch 'main' into threads-with-friends
MattyTheHacker Mar 17, 2025
02d48fe
Merge branch 'main' into threads-with-friends
MattyTheHacker Mar 18, 2025
d33d3f2
Merge branch 'main' into threads-with-friends
MattyTheHacker Mar 30, 2025
c95eb91
Fixes
MattyTheHacker Mar 30, 2025
dd9ad81
Improve logic
MattyTheHacker Mar 30, 2025
c69ea0c
use ctx.channel directly
MattyTheHacker Mar 30, 2025
2c7fc5c
Update config.py
MattyTheHacker Mar 30, 2025
c2669ea
Update cogs/add_users_to_threads_and_channels.py
MattyTheHacker Mar 30, 2025
22aab60
Fix method call
MattyTheHacker Mar 30, 2025
bab4957
improve method flow
MattyTheHacker Mar 31, 2025
e07c5c8
fix mypy errors
MattyTheHacker Mar 31, 2025
d600493
Merge branch 'main' into threads-with-friends
MattyTheHacker Mar 31, 2025
1cd8418
fix bad loop exit logic
MattyTheHacker Mar 31, 2025
e537f75
Update cogs/add_users_to_threads_and_channels.py
MattyTheHacker Mar 31, 2025
2dadbbe
Update cogs/add_users_to_threads_and_channels.py
MattyTheHacker Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from typing import TYPE_CHECKING

from .add_users_to_threads_and_channels import AddUsersToThreadsAndChannelsCommandCog
from .annual_handover_and_reset import (
AnnualRolesResetCommandCog,
AnnualYearChannelsIncrementCommandCog,
Expand Down Expand Up @@ -46,6 +47,7 @@
from utils import TeXBot, TeXBotBaseCog

__all__: "Sequence[str]" = (
"AddUsersToThreadsAndChannelsCommandCog",
"AnnualRolesResetCommandCog",
"AnnualYearChannelsIncrementCommandCog",
"ArchiveCommandCog",
Expand Down Expand Up @@ -84,6 +86,7 @@
def setup(bot: "TeXBot") -> None:
"""Add all the cogs to the bot, at bot startup."""
cogs: Iterable[type[TeXBotBaseCog]] = (
AddUsersToThreadsAndChannelsCommandCog,
AnnualRolesResetCommandCog,
AnnualYearChannelsIncrementCommandCog,
ArchiveCommandCog,
Expand Down
309 changes: 309 additions & 0 deletions cogs/add_users_to_threads_and_channels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
"""Contains Cog classes for adding users and roles to threads."""

import logging
from collections.abc import Iterable
from typing import TYPE_CHECKING

import discord

from config import settings
from exceptions import GuestRoleDoesNotExistError, GuildDoesNotExistError
from utils import TeXBotBaseCog
from utils.error_capture_decorators import capture_guild_does_not_exist_error

if TYPE_CHECKING:
from collections.abc import Sequence
from collections.abc import Set as AbstractSet
from logging import Logger
from typing import Final

from utils import TeXBotApplicationContext, TeXBotAutocompleteContext


__all__: "Sequence[str]" = ("AddUsersToThreadsAndChannelsCommandCog",)


logger: "Final[Logger]" = logging.getLogger("TeX-Bot")


class AddUsersToThreadsAndChannelsCommandCog(TeXBotBaseCog):
"""Cog for adding users to threads."""

@staticmethod
async def autocomplete_get_members(
ctx: "TeXBotAutocompleteContext",
) -> "AbstractSet[discord.OptionChoice] | AbstractSet[str]":
"""Autocomplete callable that generates the set of available selectable members."""
try:
main_guild: discord.Guild = ctx.bot.main_guild
guest_role: discord.Role = await ctx.bot.guest_role
except (GuildDoesNotExistError, GuestRoleDoesNotExistError):
return set()

members: set[discord.Member] = {
member
for member in main_guild.members
if not member.bot and guest_role in member.roles
}

if not ctx.value or ctx.value.startswith("@"):
return {
discord.OptionChoice(name=f"@{member.name}", value=str(member.id))
for member in members
}

return {
discord.OptionChoice(name=member.name, value=str(member.id)) for member in members
}

@staticmethod
async def autocomplete_get_roles(
ctx: "TeXBotAutocompleteContext",
) -> "AbstractSet[discord.OptionChoice] | AbstractSet[str]":
"""Autocomplete callable that generates the set of available selectable roles."""
try:
main_guild: discord.Guild = ctx.bot.main_guild
except GuildDoesNotExistError:
return set()

if not ctx.value or ctx.value.startswith("@"):
return {
discord.OptionChoice(name=f"@{role.name}", value=str(role.id))
for role in main_guild.roles
}

return {
discord.OptionChoice(name=role.name, value=str(role.id))
for role in main_guild.roles
}

async def add_users_or_roles_silently(
self,
users_or_roles: discord.Member
| discord.Role
| Iterable[discord.Member]
| Iterable[discord.Role],
thread: discord.Thread,
) -> None:
"""Add a user or role to a thread without pinging them."""
if isinstance(users_or_roles, Iterable):
user_or_role: discord.Role | discord.Member
for user_or_role in users_or_roles:
await self.add_users_or_roles_silently(
users_or_roles=user_or_role, thread=thread
)
return

message: discord.Message = await thread.send(
content=f"Adding {users_or_roles!r} to thread...",
silent=True,
)
await message.edit(content=f"{users_or_roles.mention}")
await message.delete(delay=1)

async def add_users_or_roles_with_ping(
self,
users_or_roles: discord.Member
| discord.Role
| list[discord.Member]
| list[discord.Role],
thread: discord.Thread,
) -> None:
"""Add a user or role to a thread and ping them."""
if isinstance(users_or_roles, Iterable):
user_or_role: discord.Role | discord.Member
for user_or_role in users_or_roles:
await self.add_users_or_roles_with_ping(
users_or_roles=user_or_role, thread=thread
)
return

if isinstance(users_or_roles, discord.Member):
try:
await thread.add_user(user=users_or_roles)
except discord.NotFound:
logger.debug(
"User: %s has blocked the bot and "
"therefore could not be added to thread: %s.",
users_or_roles,
thread,
)
return

if isinstance(users_or_roles, discord.Role):
member: discord.Member
for member in users_or_roles.members:
try:
await thread.add_user(member)
except discord.NotFound:
logger.debug(
"User: %s has blocked the bot and "
"therefore could not be added to thread: %s.",
member,
thread,
)
return
Comment thread
MattyTheHacker marked this conversation as resolved.
Outdated

@TeXBotBaseCog.listener()
@capture_guild_does_not_exist_error
async def on_thread_create(self, thread: discord.Thread) -> None:
"""Add users to a thread when it is created."""
# NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent
committee_role: discord.Role = await self.bot.committee_role
committee_elect_role: discord.Role = await self.bot.committee_elect_role

if (
thread.parent is None
or thread.parent.category is None
or "committee" not in thread.parent.category.name.lower()
or not settings["AUTO_ADD_COMMITTEE_TO_THREADS"]
):
return

await self.add_users_or_roles_silently(
users_or_roles=[committee_role, committee_elect_role], thread=thread
Comment thread
MattyTheHacker marked this conversation as resolved.
Outdated
)

@discord.slash_command( # type: ignore[no-untyped-call, misc]
name="add_users_to_channel",
description="Adds selected users to a channel.",
Comment thread
MattyTheHacker marked this conversation as resolved.
Outdated
)
@discord.option( # type: ignore[no-untyped-call, misc]
name="user",
description="The user to add to the channel.",
input_type=str,
autocomplete=discord.utils.basic_autocomplete(autocomplete_get_members), # type: ignore[arg-type]
required=True,
parameter_name="user_id_str",
)
@discord.option( # type: ignore[no-untyped-call, misc]
name="silent",
description="Whether the users being added should be pinged or not.",
input_type=bool,
required=False,
parameter_name="silent",
)
async def add_user_to_channel( # type: ignore[misc]
self,
ctx: "TeXBotApplicationContext",
user_id_str: str,
silent: bool, # noqa: FBT001
) -> None:
"""Add users or roles to a channel."""
if not isinstance(ctx.channel, (discord.TextChannel, discord.Thread)):
await self.command_send_error(
ctx=ctx,
message="This command currently only supports text channels or threads.",
)
return

try:
user_to_add: discord.Member = await self.bot.get_member_from_str_id(user_id_str)
except ValueError:
logger.debug("User ID: %s is not a valid ID.", user_id_str)
await ctx.respond(content=f"The user: {user_id_str} is not valid.")
return

if isinstance(ctx.channel, discord.TextChannel):
await ctx.channel.set_permissions(
target=user_to_add,
read_messages=True,
send_messages=True,
reason=f"User {ctx.user} used TeX-Bot slash-command `add_user_to_channel`.",
)

if not silent:
await ctx.channel.send(
content=f"{user_to_add.mention} has been added to the channel."
)

if isinstance(ctx.channel, discord.Thread):
if silent:
await self.add_users_or_roles_silently(user_to_add, ctx.channel)
else:
await self.add_users_or_roles_with_ping(user_to_add, ctx.channel)

await ctx.respond(
content=(
f"Successfully added {user_to_add.mention} "
f"to the channel: {ctx.channel.mention}."
)
)

@discord.slash_command( # type: ignore[no-untyped-call, misc]
name="add_role_to_channel",
description="Adds the selected role and it's users to a channel.",
Comment thread
MattyTheHacker marked this conversation as resolved.
Outdated
)
@discord.option( # type: ignore[no-untyped-call, misc]
name="role",
description="The role to add to the channel.",
input_type=str,
autocomplete=discord.utils.basic_autocomplete(autocomplete_get_roles), # type: ignore[arg-type]
required=True,
parameter_name="role_id_str",
)
@discord.option( # type: ignore[no-untyped-call, misc]
name="silent",
description="Whether the users being added should be pinged or not.",
input_type=bool,
required=False,
parameter_name="silent",
)
async def add_role_to_channel( # type: ignore[misc]
self,
ctx: "TeXBotApplicationContext",
role_id_str: str,
silent: bool, # noqa: FBT001
) -> None:
"""Command to add a role to a channel."""
if not isinstance(ctx.channel, discord.Thread) and not isinstance(
ctx.channel, discord.TextChannel
):
await self.command_send_error(
ctx=ctx, message="This command can only be used in a text channel or thread."
)
return

channel: discord.TextChannel | discord.Thread = ctx.channel
Comment thread
MattyTheHacker marked this conversation as resolved.
Outdated

main_guild: discord.Guild = ctx.bot.main_guild

try:
role_id: int = int(role_id_str)
except ValueError:
logger.debug("Role ID: %s is not a valid ID.", role_id_str)
await ctx.respond(content=f"The role: {role_id_str} is not valid.")
return

role_to_add: discord.Role | None = discord.utils.get(main_guild.roles, id=role_id)

if role_to_add is None:
await self.command_send_error(
ctx=ctx,
message=f"The role: <@{role_id}> is not valid or couldn't be found.",
)
return

if isinstance(channel, discord.TextChannel):
await channel.set_permissions(
target=role_to_add,
read_messages=True,
send_messages=True,
reason=f"User {ctx.user} used TeX-Bot slash-command `add_role_to_channel`.",
)

if not silent:
await channel.send(
content=f"{role_to_add.mention} has been added to the channel."
)

if isinstance(channel, discord.Thread):
if silent:
await self.add_users_or_roles_silently(role_to_add, channel)
else:
await self.add_users_or_roles_with_ping(role_to_add, channel)

await ctx.respond(
content=f"Role {role_to_add.mention} has been added to the channel.",
ephemeral=True,
)
17 changes: 17 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,22 @@ def _setup_strike_performed_manually_warning_location(cls) -> None:
raw_strike_performed_manually_warning_location
)

@classmethod
def _setup_add_committee_to_threads(cls) -> None:
Comment thread
MattyTheHacker marked this conversation as resolved.
Outdated
raw_auto_add_committee_to_threads: str = str(
os.getenv("AUTO_ADD_COMMITTEE_TO_THREADS", "True")
).lower()

if raw_auto_add_committee_to_threads not in TRUE_VALUES | FALSE_VALUES:
INVALID_AUTO_ADD_COMMITTEE_TO_THREADS_MESSAGE: Final[str] = (
"AUTO_ADD_COMMITTEE_TO_THREADS must be a boolean value."
)
raise ImproperlyConfiguredError(INVALID_AUTO_ADD_COMMITTEE_TO_THREADS_MESSAGE)

cls._settings["AUTO_ADD_COMMITTEE_TO_THREADS"] = (
raw_auto_add_committee_to_threads in TRUE_VALUES
)

@classmethod
def _setup_env_variables(cls) -> None:
"""
Expand Down Expand Up @@ -715,6 +731,7 @@ def _setup_env_variables(cls) -> None:
cls._setup_statistics_roles()
cls._setup_moderation_document_url()
cls._setup_strike_performed_manually_warning_location()
cls._setup_add_committee_to_threads()

cls._is_env_variables_setup = True

Expand Down