From cbf27070d28e43b7b6ca8c9425411abc0f00e64f Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Sun, 8 Jun 2025 11:28:38 +0200 Subject: [PATCH 1/3] configure-guild.py: Add voice channel support --- scripts/configure-guild.py | 56 ++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/scripts/configure-guild.py b/scripts/configure-guild.py index 5a0e0b4..f8be7da 100644 --- a/scripts/configure-guild.py +++ b/scripts/configure-guild.py @@ -100,6 +100,14 @@ "kick_members", "ban_members", "administrator", + "connect", + "speak", + "stream", + "use_soundboard", + "use_voice_activation", + "priority_speaker", + "deafen_members", + "mute_members", ] @@ -138,9 +146,18 @@ class TextChannel(BaseModel): channel_messages: list[MultilineString] = Field(default_factory=list) +class VoiceChannel(BaseModel): + type: Literal["voice"] = "voice" + + name: str + permission_overwrites: list[PermissionOverwrite] = Field(default_factory=list) + + class Category(BaseModel): name: str - channels: list[Annotated[TextChannel | ForumChannel, Field(discriminator="type")]] + channels: list[ + Annotated[TextChannel | ForumChannel | VoiceChannel, Field(discriminator="type")] + ] permission_overwrites: list[PermissionOverwrite] = Field(default_factory=list) @@ -754,10 +771,14 @@ def get_forum(self, name: str) -> discord.ForumChannel: raise RuntimeError(f"Could not find forum with name '{name}'") return channel - def get_channel(self, name: str) -> discord.TextChannel | discord.ForumChannel: + def get_channel( + self, name: str + ) -> discord.TextChannel | discord.ForumChannel | discord.VoiceChannel: channel = discord_get(self.guild.channels, name=name) - if channel is None or not isinstance(channel, (discord.TextChannel, discord.ForumChannel)): - raise RuntimeError(f"Could not find text or forum channel with name '{name}'") + if channel is None or not isinstance( + channel, (discord.TextChannel, discord.ForumChannel, discord.VoiceChannel) + ): + raise RuntimeError(f"Could not find text, forum, or voice channel with name '{name}'") return channel def get_role(self, name: str) -> discord.Role: @@ -774,7 +795,7 @@ def get_category(self, name: str) -> discord.CategoryChannel: async def ensure_channel_permissions( self, - channel: discord.TextChannel | discord.ForumChannel, + channel: discord.TextChannel | discord.ForumChannel | discord.VoiceChannel, permission_overwrite_templates: list[PermissionOverwrite], ) -> None: logger.info("Ensure permissions for channel %s", channel.name) @@ -830,6 +851,10 @@ async def ensure_categories_and_channels(self, category_templates: list[Category await self.ensure_text_channel( channel_template.name, category=category, position=channel_position ) + elif isinstance(channel_template, VoiceChannel): + await self.ensure_voice_channel( + channel_template.name, category=category, position=channel_position + ) elif isinstance(channel_template, ForumChannel): await self.ensure_forum_channel( channel_template.name, @@ -873,6 +898,23 @@ async def ensure_text_channel( logger.debug("Update position") await channel.edit(position=position) + async def ensure_voice_channel( + self, name: str, *, category: discord.CategoryChannel | None, position: int + ) -> None: + logger.info("Ensure voice channel %s at position %d", name, position) + channel = discord_get(self.guild.voice_channels, name=name) + if channel is None: + logger.debug("Create voice channel %s", name) + await self.guild.create_voice_channel(name=name, category=category, position=position) + else: + logger.debug("Found voice channel") + if channel.category != category: + logger.debug("Update category") + await channel.edit(category=category) + if channel.position != position: + logger.debug("Update position") + await channel.edit(position=position) + async def ensure_forum_channel( self, name: str, @@ -1007,7 +1049,11 @@ async def ensure_channel_topics(self, category_templates: list[Category]) -> Non logger.info("Ensure channel topics") for category_template in category_templates: for channel_template in category_template.channels: + if isinstance(channel_template, VoiceChannel): + continue # voice channels have no topic channel = self.get_channel(channel_template.name) + if isinstance(channel, discord.VoiceChannel): + continue # voice channels have no topic expected_topic = channel_template.topic if channel.topic != expected_topic: logger.debug("Update topic of channel %s", channel_template.name) From ee008c1de76d043730a94c97e29e15005c5290b8 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Sun, 8 Jun 2025 11:29:34 +0200 Subject: [PATCH 2/3] Fix mypy findings --- scripts/configure-guild.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/configure-guild.py b/scripts/configure-guild.py index f8be7da..0c10f16 100644 --- a/scripts/configure-guild.py +++ b/scripts/configure-guild.py @@ -10,7 +10,7 @@ import sys import textwrap from collections import defaultdict -from typing import Annotated, Literal +from typing import Annotated, Any, Literal import discord from discord import VerificationLevel @@ -170,7 +170,7 @@ class GuildConfig(BaseModel): @model_validator(mode="after") def verify_system_channel_names(self) -> Self: - channel_names = [] + channel_names: list[str] = [] for category in self.categories: channel_names.extend(channel.name for channel in category.channels) @@ -1122,7 +1122,7 @@ async def ensure_community_feature( await self.guild.edit(verification_level=discord.VerificationLevel.medium) if self.guild.default_notifications != discord.NotificationLevel.only_mentions: - self.guild.edit(default_notifications=discord.NotificationLevel.only_mentions) + await self.guild.edit(default_notifications=discord.NotificationLevel.only_mentions) if "COMMUNITY" not in self.guild.features: logger.debug("Enable guild 'COMMUNITY' feature") @@ -1151,7 +1151,7 @@ async def on_ready(self) -> None: await self.close() - async def on_error(self, event: str, /, *args, **kwargs) -> None: # noqa: ANN002,ANN003 (types) + async def on_error(self, event: str, /, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 (Any) """Event handler for uncaught exceptions.""" exc_type, exc_value, _exc_traceback = sys.exc_info() if exc_type is None: From 1d40a2f066d04299615325a153cc8d1e1c7d3ce2 Mon Sep 17 00:00:00 2001 From: Niklas Mertsch Date: Sun, 8 Jun 2025 11:30:13 +0200 Subject: [PATCH 3/3] Add 'Remote Attendees' channels --- scripts/configure-guild.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/scripts/configure-guild.py b/scripts/configure-guild.py index 0c10f16..4a23851 100644 --- a/scripts/configure-guild.py +++ b/scripts/configure-guild.py @@ -234,7 +234,13 @@ def verify_permission_roles(self) -> Self: color=DARK_ORANGE, hoist=True, mentionable=True, - permissions=["kick_members", "ban_members"], + permissions=[ + "kick_members", + "ban_members", + "priority_speaker", + "deafen_members", + "mute_members", + ], ), Role( name=ROLE_MODERATORS, @@ -246,6 +252,9 @@ def verify_permission_roles(self) -> Self: "moderate_members", "manage_messages", "manage_threads", + "priority_speaker", + "deafen_members", + "mute_members", ], ), Role( @@ -295,6 +304,9 @@ def verify_permission_roles(self) -> Self: "add_reactions", "read_message_history", "use_application_commands", + "connect", + "speak", + "use_voice_activation", ], ), ], @@ -527,6 +539,17 @@ def verify_permission_roles(self) -> Self: PermissionOverwrite(roles=ROLES_REGISTERED, allow=["view_channel"]), ], ), + Category( + name="Remote Attendees", + channels=[ + TextChannel(name="remote-text", topic="Text chat for remote attendees"), + VoiceChannel(name="remote-voice"), + ], + permission_overwrites=[ + PermissionOverwrite(roles=[ROLE_EVERYONE], deny=["view_channel"]), + PermissionOverwrite(roles=ROLES_REGISTERED, allow=["view_channel"]), + ], + ), Category( name="Conference Organization", channels=[