Skip to content

discord_lfg.group_builder

Main DB instance control.

logger = logging.getLogger(__name__) module-attribute #

EditCreatorRole(open_roles) #

Bases: Select

Creator role selector.

Initialisation.

Source code in src\discord_lfg\group_builder.py
def __init__(self, open_roles: list[str]):
    """Initialisation."""
    options = [discord.SelectOption(label=f"{role_name}") for role_name in open_roles]
    disabled = False
    placeholder = "Choose the role you want to swap to"
    if len(options) == 0:
        disabled = True
        options = [discord.SelectOption(label="placeholder")]
        placeholder = "All roles are full"

    super().__init__(
        placeholder=placeholder,
        min_values=1,
        max_values=1,
        options=options,
        row=2,
        required=False,
        disabled=disabled,
    )

callback(interaction) async #

Does the thing.

Source code in src\discord_lfg\group_builder.py
async def callback(self, interaction: discord.Interaction):
    """Does the thing."""
    assert self.view is not None
    logger.debug(f"EditCreatorRole callback {self.view.group_builder.group_title}")
    if self.values:
        self.view.new_creator_role = self.values[0]
    await interaction.response.defer()

EditRemoveUser(users) #

Bases: Select

Select which users to remove.

Initialisation.

Source code in src\discord_lfg\group_builder.py
def __init__(self, users: dict[int, GroupUser]):
    """Initialisation."""
    disabled = True
    options = [discord.SelectOption(label="placeholder")]
    if len(users) > 0:
        options = [
            discord.SelectOption(
                label=f"{group_user.role.capitalize()}: {group_user.display_name}",
                value=f"{user_id}",
            )
            for user_id, group_user in users.items()
        ]
        disabled = False

    super().__init__(
        placeholder="Choose people to remove from the group",
        min_values=1,
        max_values=len(options),
        options=options,
        row=0,
        required=False,
        disabled=disabled,
    )

callback(interaction) async #

Does the thing.

Source code in src\discord_lfg\group_builder.py
async def callback(self, interaction: discord.Interaction):
    """Does the thing."""
    assert self.view is not None
    logger.debug(f"EditRemoveUser callback {self.view.group_builder.group_title}")
    logger.debug(f"Selected for removal: {self.values}")
    self.view.remove_users = []
    for user_id in self.values:
        user_id = int(user_id)
        self.view.remove_users.append(self.view.group_builder.get_user_by_id(user_id))
    logger.debug(f"Recorded for removal: {self.view.remove_users}")
    await interaction.response.defer()

EditRemoveUserReason(users, kick_reasons) #

Bases: Select

Provide a reason for removing users.

Initialisation.

Source code in src\discord_lfg\group_builder.py
def __init__(self, users: dict[int, GroupUser], kick_reasons: list[str]):
    """Initialisation."""
    disabled = True
    options = [discord.SelectOption(label="placeholder")]
    if len(users) > 0 and any([user > 0 for user in users]):
        reasons = kick_reasons
        options = [discord.SelectOption(label=reason) for reason in reasons]
        disabled = False

    super().__init__(
        placeholder="Choose a reason for removing people",
        min_values=1,
        max_values=1,
        options=options,
        row=1,
        required=False,
        disabled=disabled,
    )

callback(interaction) async #

Does the thing.

Source code in src\discord_lfg\group_builder.py
async def callback(self, interaction: discord.Interaction):
    """Does the thing."""
    assert self.view is not None
    logger.debug(f"EditRemoveUserReason callback {self.view.group_builder.group_title}")
    if self.values:
        self.view.remove_users_reason = self.values[0]
    await interaction.response.defer()

GroupBuilder(interaction, group_info, config, creator_role, filled_spots) #

Builds a group dynamically.

Creates a Group Builder.

PARAMETER DESCRIPTION
interaction

The discord interaction which created this GroupBuilder. This allows us to capture the user information depending on who created this instance.

TYPE: Interaction

group_info

A dictionary of the group specific information: see GroupDetails

TYPE: dict

config

A dictionary of configuration information for Group Builder, all optional extras

TYPE: CommandConfig

creator_role

The role the creator has chosen (must match a role name)

TYPE: str

filled_spots

A dictionary of which roles are already filled in the format {name: count}

TYPE: dict[str, int]

Source code in src\discord_lfg\group_builder.py
def __init__(
    self,
    interaction: discord.Interaction,
    group_info: dict,
    config: CommandConfig,
    creator_role: str,
    filled_spots: dict[str, int],
):
    """Creates a Group Builder.

    Args:
        interaction: The discord interaction which created this GroupBuilder. This allows
            us to capture the user information depending on who created this instance.
        group_info: A dictionary of the group specific information: see `GroupDetails`
        config: A dictionary of configuration information for Group Builder, all optional extras
        creator_role: The role the creator has chosen (must match a role name)
        filled_spots: A dictionary of which roles are already filled in the format {name: count}
    """
    logger.debug(
        f"GroupBuilder created by {interaction.user.id} {interaction.user.display_name}"
    )
    self.group_editor: None | GroupEditOptions = None
    self.role_counts = {role.name: role.count for role in config.roles.values()}
    command_name = config.name
    guild_name = config.guild_name
    timeout_length = config.timeout_length
    editable_length = config.editable_length
    debug = config.debug
    guild_roles = config.guild_roles
    kick_reasons = config.kick_reasons

    self._state_init(command_name, guild_name, timeout_length, editable_length, debug)
    self._setup_group(**group_info, guild_name=guild_name, kick_reasons=kick_reasons)
    self._roles_init(
        config.roles,
        guild_roles,
        interaction.channel.name if isinstance(interaction.channel.name, str) else "",  # type: ignore
        config.channel_role_mentions,
    )

    self.creator = self.create_user_from_interaction(interaction, creator_role, True)
    self.add_role(creator_role, self.creator)
    self.kicked_users: list[GroupUser] = []
    self.fill_spots(filled_spots)
    logger.debug(
        f"GroupBuilder initialisation finished for {self.listing_message} {self.group_title}"
    )

creator = self.create_user_from_interaction(interaction, creator_role, True) instance-attribute #

current_role_tags property #

Retrieves a string tagging all current required roles listed in the group.

current_user_ids property #

Retrieves the current valid user IDs in the instance.

current_user_tags property #

Retrieves a string tagging all current users listed in the group.

description property #

Gets a standardised description for the group including role spots.

filled_roles property #

Gets a string indicating the roles that have been filled, as emojis.

group_buttons property #

A set of buttons for manipulating the group while it's open.

group_editor = None instance-attribute #

group_embed property #

Gets a Discord Embed of the current group user state.

group_title property #

Gets a standardised title string for the group.

kicked_users = [] instance-attribute #

listing_message property #

Gets the listing message for the group.

listing_message_body property #

Body of the listing message.

passphrase property #

Retrieves the passphrase for this group.

role_counts = {(role.name): (role.count) for role in (config.roles.values())} instance-attribute #

add_role(assigned_role, group_user, filled_spot=False) #

Update the specified role name with the given user ID and display name.

Source code in src\discord_lfg\group_builder.py
def add_role(self, assigned_role: str, group_user: GroupUser, filled_spot: bool = False):
    """Update the specified role name with the given user ID and display name."""
    # a user can only be present in a group once,
    # so must be removed if present before being added.
    if not filled_spot:
        for role_name in self.role_counts:
            remove_role = self.roles[role_name]
            if group_user.id in [user.id for user in remove_role.users]:
                self.remove_role(remove_role, group_user.id)

    role = self.roles[assigned_role]
    logger.debug(
        f"add_role {self.group_title}\nrole:\n{role}\nid: {group_user.id}\nstate: {self.state}"
    )
    role_idx = role.assigned.index(False)
    role.users[role_idx] = (
        self._create_filled_spot_user(role.name) if filled_spot else group_user
    )
    role.assigned[role_idx] = True
    if all(role.assigned):
        role.disabled = True
    self.state.empty_spots -= 1

cancel_group() async #

Cancels the group and informs all current signups that it's been cancelled.

Source code in src\discord_lfg\group_builder.py
async def cancel_group(self):
    """Cancels the group and informs all current signups that it's been cancelled."""
    logger.debug(
        f"{self.group_title} cancelled by {self.creator.id} / {self.creator.display_name}"
    )
    self.state.cancelled = True
    await self.edit_message()
    await self.message.channel.send(content=self.listing_message)
    self._record_group(finished_state="cancelled")
    del self

create_user_from_interaction(interaction, role, creator=False) #

Creates a GroupUser from a given discord interaction.

Source code in src\discord_lfg\group_builder.py
def create_user_from_interaction(
    self, interaction: discord.Interaction, role: str, creator: bool = False
):
    """Creates a GroupUser from a given discord interaction."""
    return GroupUser(
        id=interaction.user.id,
        tag=f"<@{interaction.user.id}>",
        name=interaction.user.name,
        display_name=interaction.user.display_name,
        global_name=interaction.user.global_name,
        interaction=interaction,
        creator=creator,
        role=role,
    )

edit_message() async #

Updates the Discord displayed message based on the current status of the group.

Source code in src\discord_lfg\group_builder.py
async def edit_message(self):
    """Updates the Discord displayed message based on the current status of the group."""
    logger.debug("edit_message")
    if self.message is not None:
        await self.message.edit(**self._message_content)
    else:
        return "self.message is not initialised", self._message_content

fill_spots(filled_spots) #

Fills spots in the listing based on the filled spots dictionary given.

Source code in src\discord_lfg\group_builder.py
def fill_spots(self, filled_spots: dict[str, int]):
    """Fills spots in the listing based on the filled spots dictionary given."""
    for role_name, num_filled in filled_spots.items():
        for _ in range(num_filled):
            self.state.filled_spots += 1
            self.add_role(
                assigned_role=role_name,
                group_user=self._create_filled_spot_user(role_name),
                filled_spot=True,
            )

get_role_by_id(user_id) #

Retrieves a user from the roles using their id.

Source code in src\discord_lfg\group_builder.py
def get_role_by_id(self, user_id: int) -> GroupRole:
    """Retrieves a user from the roles using their id."""
    for role in self.roles.values():
        for user in role.users:
            if user_id == user.id:
                return role
    raise ValueError("get_role_by_id was given user not in the current group")

get_user_by_id(user_id) #

Retrieves a user from the roles using their id.

Source code in src\discord_lfg\group_builder.py
def get_user_by_id(self, user_id: int) -> GroupUser:
    """Retrieves a user from the roles using their id."""
    for role in self.roles.values():
        for user in role.users:
            if user_id == user.id:
                return user
    raise ValueError("get_user_by_id was given user not in the current group")

is_closed() #

Checks if the group should be closed or re-opened and sets a timer accordingly.

Source code in src\discord_lfg\group_builder.py
def is_closed(self):
    """Checks if the group should be closed or re-opened and sets a timer accordingly."""
    if self.state.empty_spots == 0 and not self.state.closed:
        logger.debug(f"{self.listing_message} {self.group_title} closed as it is full")
        self.state.closed = True
        self.state.close_group_at = datetime_now_utc() + timedelta(
            minutes=self.state.editable_length
        )
        logger.debug(f"group closed but editable until {self.state.close_group_at}")
    elif self.state.empty_spots > 0 and self.state.closed:
        logger.debug(f"{self.listing_message} {self.group_title} reopened as it has space")
        self.state.closed = False
        self.state.close_group_at = datetime_now_utc() + timedelta(
            minutes=self.state.editable_length
        )
        logger.debug(f"group reopened and editable until {self.state.close_group_at}")

remove_filled_spot(user) #

Removes a filled spot from the given role.

Source code in src\discord_lfg\group_builder.py
def remove_filled_spot(self, user: GroupUser):
    """Removes a filled spot from the given role."""
    self.state.filled_spots -= 1
    role = self.roles[user.role]
    self.remove_role(role, user.id)

remove_role(role, id) #

Removes the role from the given user.

Source code in src\discord_lfg\group_builder.py
def remove_role(self, role: GroupRole, id: int):
    """Removes the role from the given user."""
    logger.debug(f"remove_role {self.group_title}\nrole: {role}\nid: {id}\nstate: {self.state}")
    role_idx = [user.id for user in role.users].index(id)
    role.users[role_idx] = self._create_empty_spot_user(role.name)
    role.assigned[role_idx] = False
    role.disabled = False
    self.state.empty_spots += 1

role_info(role_name) #

Gets information about the requested role.

Source code in src\discord_lfg\group_builder.py
def role_info(self, role_name):
    """Gets information about the requested role."""
    if role_name in self.roles:
        return self.roles[role_name]
    else:
        raise ValueError(f"{role_name} not in roles: {list(self.roles.keys())}")

send_message(interaction) async #

Sends the initial message for the Group Builder.

Source code in src\discord_lfg\group_builder.py
async def send_message(self, interaction: discord.Interaction):
    """Sends the initial message for the Group Builder."""
    self.message = await interaction.channel.send(**self._message_content)  # type: ignore
    self._task = asyncio.create_task(self._check_if_closed_or_timed_out())

send_passphrase(interaction) async #

Sends the passphrase.

Source code in src\discord_lfg\group_builder.py
async def send_passphrase(self, interaction: discord.Interaction):
    """Sends the passphrase."""
    logger.debug(
        f"send_passphrase {self.group_title}\n"
        f"user_id: {interaction.user.id}\n"
        f"display_name: {interaction.user.display_name}\n"
        f"passphrase: {self.passphrase}"
    )
    message_func = (
        interaction.followup.send
        if interaction.response.is_done()
        else interaction.response.send_message
    )
    await message_func(
        content=f"The passphrase for your group is: {self.passphrase}", ephemeral=True
    )

GroupDetails(activity_name, listed_as, creator_notes, extra_info, kick_reasons) dataclass #

Container for group details.

activity_name instance-attribute #

creator_notes instance-attribute #

extra_info instance-attribute #

kick_reasons instance-attribute #

listed_as instance-attribute #

GroupEditOptions(group_builder) #

Bases: View

LFG options menu.

Initialisation.

Source code in src\discord_lfg\group_builder.py
def __init__(self, group_builder: GroupBuilder):
    """Initialisation."""
    super().__init__(timeout=60)
    self.message: discord.InteractionMessage = None  # type: ignore
    self.interaction: discord.Interaction = None  # type: ignore
    self.group_builder = group_builder
    self.filled_spot_name = self.group_builder.state.filled_spot_name
    self.remove_users_reason = ""
    self.confirmed = False
    self.cancel_group_state = 0
    self.remove_users: list[GroupUser] = []

    removeable_users = {}
    for role_name, role_item in self.group_builder.roles.items():
        for user in role_item.users:
            if user.creator:
                self.creator_role = role_name
            elif user.id > -100:
                removeable_users[user.id] = user
    self.new_creator_role = self.creator_role
    open_roles = [
        role.name
        for role in self.group_builder.roles.values()
        if not (all(role.assigned) or role.name == self.creator_role)
    ]
    kick_reasons = self.group_builder.group_details.kick_reasons

    self.edit_remove_users = EditRemoveUser(removeable_users)
    self.add_item(self.edit_remove_users)

    self.edit_remove_users_reason = EditRemoveUserReason(removeable_users, kick_reasons)
    self.add_item(self.edit_remove_users_reason)

    if len(open_roles) > 0:
        self.edit_creator_role = EditCreatorRole(open_roles)
        self.add_item(self.edit_creator_role)

cancel_group_state = 0 instance-attribute #

confirmed = False instance-attribute #

creator_role = role_name instance-attribute #

edit_creator_role = EditCreatorRole(open_roles) instance-attribute #

edit_remove_users = EditRemoveUser(removeable_users) instance-attribute #

edit_remove_users_reason = EditRemoveUserReason(removeable_users, kick_reasons) instance-attribute #

filled_spot_name = self.group_builder.state.filled_spot_name instance-attribute #

group_builder = group_builder instance-attribute #

interaction = None instance-attribute #

message = None instance-attribute #

new_creator_role = self.creator_role instance-attribute #

remove_users = [] instance-attribute #

remove_users_reason = '' instance-attribute #

cancel_edit(interaction, button) async #

Cancel the menu.

Source code in src\discord_lfg\group_builder.py
@discord.ui.button(label="Abort changes", style=discord.ButtonStyle.secondary, row=4)
async def cancel_edit(self, interaction: discord.Interaction, button: discord.ui.Button):
    """Cancel the menu."""
    self.confirmed = False
    await self.message.edit(content="Group editing cancelled.", view=None)  # type: ignore
    self.group_builder.group_editor = None
    self.stop()

cancel_group(interaction, button) async #

Cancel the menu.

Source code in src\discord_lfg\group_builder.py
@discord.ui.button(label="Cancel group", style=discord.ButtonStyle.danger, row=4)
async def cancel_group(self, interaction: discord.Interaction, button: discord.ui.Button):
    """Cancel the menu."""
    self.cancel_group_state += 1
    self.confirmed = False
    if self.cancel_group_state == 2:
        await self.message.edit(content="Group cancelled.", view=None)  # type: ignore
        await self.group_builder.cancel_group()
        self.group_builder.group_editor = None
        self.stop()
    else:
        await interaction.response.defer()

confirm_edit(interaction, button) async #

Confirm the edits.

Source code in src\discord_lfg\group_builder.py
@discord.ui.button(label="Confirm changes", style=discord.ButtonStyle.green, row=4)
async def confirm_edit(self, interaction: discord.Interaction, button: discord.ui.Button):
    """Confirm the edits."""
    self.confirmed = True
    is_creator_role_swapped = self._change_creator_role()
    is_removed_users = self._remove_users()
    self.group_builder.is_closed()
    await self.group_builder.edit_message()
    logger.debug(
        f"edit confirm {self.group_builder.group_title}: {is_creator_role_swapped}, {is_removed_users}"
    )

    return_content = "Editing complete."

    if is_creator_role_swapped is not None and not is_creator_role_swapped:
        logger.debug("not is_creator_role_swapped")
        await self.interaction.followup.send(
            content="The role you wanted to swap to is no longer available", ephemeral=True
        )
        await interaction.response.defer()
        return None
    elif is_creator_role_swapped is not None:
        logger.debug("is_creator_role_swapped is not None")
        return_content += (
            f"\nCreator role swapped from {self.creator_role} to {self.new_creator_role}"
        )

    if is_removed_users is not None and not is_removed_users:
        logger.debug("not is_removed_users")
        await self.interaction.followup.send(
            content="You must provide a removal reason if you are removing users",
            ephemeral=True,
        )
        await interaction.response.defer()
        return None
    elif is_removed_users is not None:
        logger.debug("is_removed_users is not None")
        users_removed_str = [
            f"- `{user.display_name}`: {user.removal_reason}" for user in self.remove_users
        ]
        users_removed_str = "\n".join(users_removed_str)
        return_content += f"\nUsers removed: \n{users_removed_str}"
        for user in self.remove_users:
            if user.id > 0:
                assert user.interaction is not None
                message_func = (
                    user.interaction.followup.send
                    if user.interaction.response.is_done()
                    else user.interaction.response.send_message
                )
                await message_func(
                    content=(
                        f"{user.tag} You have been removed from the group with the reason: "
                        f"{user.removal_reason}"
                    ),
                    ephemeral=True,
                )

    await self.message.edit(content=return_content, view=None)  # type: ignore
    self.group_builder.group_editor = None
    self.stop()

on_group_close() async #

Do stuff when the group managing this editor is closed.

Source code in src\discord_lfg\group_builder.py
async def on_group_close(self) -> None:
    """Do stuff when the group managing this editor is closed."""
    logger.debug("edit menu closed by parent GroupBuilder.")
    if self.message:
        await self.message.edit(content="Group closed, editing no longer available.", view=None)  # type: ignore
    self.group_builder.group_editor = None
    self.stop()

on_timeout() async #

Do stuff when timeout occurs.

Source code in src\discord_lfg\group_builder.py
async def on_timeout(self) -> None:
    """Do stuff when timeout occurs."""
    logger.debug("edit menu timed out.")
    if self.message:
        await self.message.edit(content="Group editing has timed out.", view=None)  # type: ignore
    self.group_builder.group_editor = None
    self.stop()

GroupRole(name, users, assigned, button_style, disabled, emoji, role_mention) dataclass #

Container for a particular role type.

assigned instance-attribute #

button_style instance-attribute #

disabled instance-attribute #

emoji instance-attribute #

name instance-attribute #

role_mention instance-attribute #

users instance-attribute #

GroupState(command_name, created_at, close_group_at, editable_length, closed, cancelled, timed_out, empty_spots, filled_spots, filled_spot_name, passphrase, debug, empty_filled_role_increment=-1) dataclass #

Container for the state of the group.

cancelled instance-attribute #

close_group_at instance-attribute #

closed instance-attribute #

command_name instance-attribute #

created_at instance-attribute #

debug instance-attribute #

editable_length instance-attribute #

empty_filled_role_increment = -1 class-attribute instance-attribute #

empty_spots instance-attribute #

filled_spot_name instance-attribute #

filled_spots instance-attribute #

passphrase instance-attribute #

timed_out instance-attribute #

GroupUser(id, tag, name, display_name, global_name, interaction, creator, role, removal_reason='') dataclass #

Container for discord user information relevant to building a group.

creator instance-attribute #

display_name instance-attribute #

global_name instance-attribute #

id instance-attribute #

interaction instance-attribute #

name instance-attribute #

removal_reason = '' class-attribute instance-attribute #

role instance-attribute #

tag instance-attribute #