Skip to content

tux.utils.emoji

Classes:

Name Description
EmojiManager

Manages application emojis, caching, and synchronization from local files.

Classes

EmojiManager(bot: commands.Bot, emojis_path: Path | None = None, create_delay: float | None = None)

Manages application emojis, caching, and synchronization from local files.

Initializes the EmojiManager.

Parameters:

Name Type Description Default
bot Bot

The discord bot instance.

required
emojis_path Optional[Path]

Path to the directory containing local emoji files. Defaults to DEFAULT_EMOJI_ASSETS_PATH.

None
create_delay Optional[float]

Delay in seconds before creating an emoji to mitigate rate limits. Defaults to DEFAULT_EMOJI_CREATE_DELAY.

None

Methods:

Name Description
init

Initializes the emoji cache by fetching application emojis.

get

Retrieves an emoji from the cache.

sync_emojis

Synchronizes emojis from the local assets directory to the application.

resync_emoji

Resyncs a specific emoji: Deletes existing, finds local file, creates new.

delete_all_emojis

Delete all application emojis that match names from the emoji assets directory.

Source code in tux/utils/emoji.py
Python
def __init__(
    self,
    bot: commands.Bot,
    emojis_path: Path | None = None,
    create_delay: float | None = None,
) -> None:
    """Initializes the EmojiManager.

    Parameters
    ----------
    bot : commands.Bot
        The discord bot instance.
    emojis_path : Optional[Path], optional
        Path to the directory containing local emoji files.
        Defaults to DEFAULT_EMOJI_ASSETS_PATH.
    create_delay : Optional[float], optional
        Delay in seconds before creating an emoji to mitigate rate limits.
        Defaults to DEFAULT_EMOJI_CREATE_DELAY.
    """

    self.bot = bot
    self.cache: dict[str, discord.Emoji] = {}
    self.emojis_path = emojis_path or DEFAULT_EMOJI_ASSETS_PATH
    self.create_delay = create_delay if create_delay is not None else DEFAULT_EMOJI_CREATE_DELAY
    self._init_lock = asyncio.Lock()
    self._initialized = False

    # If in Docker and no custom path was provided, use the Docker path
    if not emojis_path and DOCKER_EMOJI_ASSETS_PATH.exists() and DOCKER_EMOJI_ASSETS_PATH.is_dir():
        logger.info(f"Docker environment detected, using emoji path: {DOCKER_EMOJI_ASSETS_PATH}")
        self.emojis_path = DOCKER_EMOJI_ASSETS_PATH

    # Ensure the emoji path exists and is a directory
    if not self.emojis_path.is_dir():
        logger.critical(
            f"Emoji assets path is invalid or not a directory: {self.emojis_path}. "
            f"Emoji synchronization and resync features will be unavailable.",
        )

        # Do not attempt to create it. Subsequent operations that rely on this path
        # (like sync_emojis) will fail gracefully or log errors.
        # The manager itself is initialized, but operations requiring the path won't work.

    else:
        # Log path relative to project root for cleaner logs
        try:
            project_root = Path(__file__).parents[2]
            log_path = self.emojis_path.relative_to(project_root)
        except ValueError:
            log_path = self.emojis_path  # Fallback if path isn't relative
        logger.info(f"Using emoji assets directory: {log_path}")

Functions

init() -> bool async

Initializes the emoji cache by fetching application emojis.

Ensures the cache reflects the current state of application emojis on Discord. This method is locked to prevent concurrent initialization attempts.

Returns:

Type Description
bool

True if initialization was successful or already done, False otherwise.

Source code in tux/utils/emoji.py
Python
async def init(self) -> bool:
    """Initializes the emoji cache by fetching application emojis.

    Ensures the cache reflects the current state of application emojis on Discord.
    This method is locked to prevent concurrent initialization attempts.

    Returns
    -------
    bool
        True if initialization was successful or already done, False otherwise.
    """

    async with self._init_lock:
        if self._initialized:
            logger.debug("Emoji cache already initialized.")
            return True

        logger.info("Initializing emoji manager and cache...")

        try:
            app_emojis = await self.bot.fetch_application_emojis()
            self.cache = {emoji.name: emoji for emoji in app_emojis if _is_valid_emoji_name(emoji.name)}

            logger.info(f"Initialized emoji cache with {len(self.cache)} emojis.")
            self._initialized = True

        except discord.HTTPException as e:
            logger.error(f"Failed to fetch application emojis during init: {e}")
            self._initialized = False
            return False
        except Exception:
            logger.exception("Unexpected error during emoji cache initialization.")
            self._initialized = False
            return False

        else:
            return True
get(name: str) -> discord.Emoji | None

Retrieves an emoji from the cache.

Ensures initialization before attempting retrieval.

Parameters:

Name Type Description Default
name str

The name of the emoji to retrieve.

required

Returns:

Type Description
Emoji | None

The discord.Emoji object if found, None otherwise.

Source code in tux/utils/emoji.py
Python
def get(self, name: str) -> discord.Emoji | None:
    """Retrieves an emoji from the cache.

    Ensures initialization before attempting retrieval.

    Parameters
    ----------
    name : str
        The name of the emoji to retrieve.

    Returns
    -------
    discord.Emoji | None
        The discord.Emoji object if found, None otherwise.
    """

    if not self._initialized:
        logger.warning("Attempted to get emoji before cache initialization. Call await manager.init() first.")

        # Avoid deadlocks: Do not call init() here directly.
        # Rely on the initial setup_hook call.
        return None

    return self.cache.get(name)
_create_discord_emoji(name: str, image_bytes: bytes) -> discord.Emoji | None async

Internal helper to create a Discord emoji with error handling and delay.

Parameters:

Name Type Description Default
name str

The name of the emoji to create.

required
image_bytes bytes

The image bytes of the emoji to create.

required

Returns:

Type Description
Emoji | None

The newly created emoji if successful, otherwise None.

Source code in tux/utils/emoji.py
Python
async def _create_discord_emoji(self, name: str, image_bytes: bytes) -> discord.Emoji | None:
    """Internal helper to create a Discord emoji with error handling and delay.

    Parameters
    ----------
    name : str
        The name of the emoji to create.
    image_bytes : bytes
        The image bytes of the emoji to create.

    Returns
    -------
    discord.Emoji | None
        The newly created emoji if successful, otherwise None.
    """

    if not _is_valid_emoji_name(name):
        logger.error(f"Attempted to create emoji with invalid name: '{name}'")
        return None

    try:
        await asyncio.sleep(self.create_delay)
        emoji = await self.bot.create_application_emoji(name=name, image=image_bytes)
        self.cache[name] = emoji  # Update cache immediately
        logger.info(f"Successfully created emoji '{name}'. ID: {emoji.id}")
        return emoji  # noqa: TRY300

    except discord.HTTPException as e:
        logger.error(f"Failed to create emoji '{name}': {e}")
    except ValueError as e:
        logger.error(f"Invalid value for creating emoji '{name}': {e}")
    except Exception as e:
        logger.exception(f"An unexpected error occurred creating emoji '{name}': {e}")

    return None
_process_emoji_file(file_path: Path) -> tuple[discord.Emoji | None, Path | None] async

Attempts to process a single emoji file.

Parameters:

Name Type Description Default
file_path Path

The path to the emoji file to process

required

Returns:

Type Description
tuple[Emoji | None, Path | None]

A tuple where the first element is the newly created emoji (if created) and the second element is the file_path if processing failed or was skipped.

Source code in tux/utils/emoji.py
Python
async def _process_emoji_file(self, file_path: Path) -> tuple[discord.Emoji | None, Path | None]:
    """Attempts to process a single emoji file.

    Parameters
    ----------
    file_path : Path
        The path to the emoji file to process

    Returns
    -------
    tuple[discord.Emoji | None, Path | None]
        A tuple where the first element is the newly created emoji (if created)
        and the second element is the file_path if processing failed or was skipped.
    """
    if not file_path.is_file():
        logger.trace(f"Skipping non-file item: {file_path.name}")
        return None, file_path

    emoji_name = file_path.stem

    if not _is_valid_emoji_name(emoji_name):
        logger.warning(f"Skipping file with invalid potential emoji name: {file_path.name}")
        return None, file_path

    if self.get(emoji_name):
        logger.trace(f"Emoji '{emoji_name}' already exists, skipping.")
        return None, file_path

    logger.debug(f"Emoji '{emoji_name}' not found in cache, attempting to create from {file_path.name}.")

    if img_bytes := _read_emoji_file(file_path):
        new_emoji = await self._create_discord_emoji(emoji_name, img_bytes)
        if new_emoji:
            return new_emoji, None

    return None, file_path  # Failed creation or read
sync_emojis() -> tuple[list[discord.Emoji], list[Path]] async

Synchronizes emojis from the local assets directory to the application.

Ensures the cache is initialized, then iterates through local emoji files. If an emoji with the same name doesn't exist in the cache, it attempts to create it.

Returns:

Type Description
tuple[list[Emoji], list[Path]]

A tuple containing: - A list of successfully created discord.Emoji objects. - A list of file paths for emojis that already existed or failed.

Source code in tux/utils/emoji.py
Python
async def sync_emojis(self) -> tuple[list[discord.Emoji], list[Path]]:
    """Synchronizes emojis from the local assets directory to the application.

    Ensures the cache is initialized, then iterates through local emoji files.
    If an emoji with the same name doesn't exist in the cache, it attempts to create it.

    Returns
    -------
    tuple[list[discord.Emoji], list[Path]]
        A tuple containing:
        - A list of successfully created discord.Emoji objects.
        - A list of file paths for emojis that already existed or failed.
    """

    if not await self._ensure_initialized():
        logger.error("Cannot sync emojis: Cache initialization failed.")
        # Attempt to list files anyway for the return value

        with contextlib.suppress(Exception):
            return [], list(self.emojis_path.iterdir())
        return [], []

    logger.info(f"Starting emoji synchronization from {self.emojis_path}...")

    duplicates_or_failed: list[Path] = []
    created_emojis: list[discord.Emoji] = []

    try:
        files_to_process = list(self.emojis_path.iterdir())
    except OSError as e:
        logger.error(f"Failed to list files in emoji directory {self.emojis_path}: {e}")
        return [], []

    if not files_to_process:
        logger.warning(f"No files found in emoji directory: {self.emojis_path}")
        return [], []

    for file_path in files_to_process:
        emoji, failed_file = await self._process_emoji_file(file_path)
        if emoji:
            created_emojis.append(emoji)
        elif failed_file:
            duplicates_or_failed.append(failed_file)

    logger.info(
        f"Emoji synchronization finished. "
        f"Created: {len(created_emojis)}, Duplicates/Skipped/Failed: {len(duplicates_or_failed)}.",
    )

    return created_emojis, duplicates_or_failed
_ensure_initialized() -> bool async

Internal helper: Checks if cache is initialized, logs warning if not.

Source code in tux/utils/emoji.py
Python
async def _ensure_initialized(self) -> bool:
    """Internal helper: Checks if cache is initialized, logs warning if not."""
    if self._initialized:
        return True
    logger.warning("Operation called before cache was initialized. Call await manager.init() first.")
    # Attempting init() again might lead to issues/deadlocks depending on context.
    # Force initialization in setup_hook.
    return False
_delete_discord_emoji(name: str) -> bool async

Internal helper: Deletes an existing Discord emoji by name and updates cache.

Parameters:

Name Type Description Default
name str

The name of the emoji to delete.

required

Returns:

Type Description
bool

True if the emoji was deleted, False otherwise.

Source code in tux/utils/emoji.py
Python
async def _delete_discord_emoji(self, name: str) -> bool:
    """Internal helper: Deletes an existing Discord emoji by name and updates cache.

    Parameters
    ----------
    name : str
        The name of the emoji to delete.

    Returns
    -------
    bool
        True if the emoji was deleted, False otherwise.
    """

    existing_emoji = self.get(name)
    if not existing_emoji:
        logger.info(f"No existing emoji '{name}' found in cache. Skipping deletion.")
        return False  # Indicate no deletion occurred

    logger.debug(f"Attempting deletion of application emoji '{name}'...")
    deleted_on_discord = False

    try:
        await existing_emoji.delete()
        logger.info(f"Successfully deleted existing application emoji '{name}'.")
        deleted_on_discord = True

    except discord.NotFound:
        logger.warning(f"Emoji '{name}' was in cache but not found on Discord for deletion.")
    except discord.Forbidden:
        logger.error(f"Missing permissions to delete application emoji '{name}'.")
    except discord.HTTPException as e:
        logger.error(f"Failed to delete application emoji '{name}': {e}")
    except Exception as e:
        logger.exception(f"An unexpected error occurred deleting emoji '{name}': {e}")

    finally:
        # Always remove from cache if it was found initially
        if self.cache.pop(name, None):
            logger.debug(f"Removed '{name}' from cache.")

    return deleted_on_discord
resync_emoji(name: str) -> discord.Emoji | None async

Resyncs a specific emoji: Deletes existing, finds local file, creates new.

Parameters:

Name Type Description Default
name str

The name of the emoji to resync.

required

Returns:

Type Description
Optional[Emoji]

The newly created emoji if successful, otherwise None.

Source code in tux/utils/emoji.py
Python
async def resync_emoji(self, name: str) -> discord.Emoji | None:
    """Resyncs a specific emoji: Deletes existing, finds local file, creates new.

    Parameters
    ----------
    name : str
        The name of the emoji to resync.

    Returns
    -------
    Optional[discord.Emoji]
        The newly created emoji if successful, otherwise None.
    """

    logger.info(f"Starting resync process for emoji: '{name}'...")

    if not await self._ensure_initialized():
        return None  # Stop if initialization failed

    # Step 1 & 2: Delete existing emoji (if any) and remove from cache
    await self._delete_discord_emoji(name)

    # Step 3: Find the local file
    local_file_path = _find_emoji_file(self.emojis_path, name)
    if not local_file_path:
        # Error logged in utility function
        logger.error(f"Resync failed for '{name}': Could not find local file.")
        return None

    # Step 4: Process the found emoji file
    new_emoji, _ = await self._process_emoji_file(local_file_path)

    if new_emoji:
        logger.info(f"Resync completed successfully for '{name}'. New ID: {new_emoji.id}")
    else:
        logger.error(f"Resync failed for '{name}' during creation step.")

    logger.info(f"Resync process for emoji '{name}' finished.")  # Log finish regardless of success
    return new_emoji
delete_all_emojis() -> tuple[list[str], list[str]] async

Delete all application emojis that match names from the emoji assets directory.

This method: 1. Ensures the emoji cache is initialized 2. Finds all potential emoji names from the assets directory 3. Deletes any matching emojis from Discord and updates the cache

Returns:

Type Description
tuple[list[str], list[str]]

A tuple containing: - A list of successfully deleted emoji names - A list of emoji names that failed to delete or weren't found

Source code in tux/utils/emoji.py
Python
async def delete_all_emojis(self) -> tuple[list[str], list[str]]:
    """Delete all application emojis that match names from the emoji assets directory.

    This method:
    1. Ensures the emoji cache is initialized
    2. Finds all potential emoji names from the assets directory
    3. Deletes any matching emojis from Discord and updates the cache

    Returns
    -------
    tuple[list[str], list[str]]
        A tuple containing:
        - A list of successfully deleted emoji names
        - A list of emoji names that failed to delete or weren't found
    """
    if not await self._ensure_initialized():
        logger.error("Cannot delete emojis: Cache initialization failed.")
        return [], []

    logger.info("Starting deletion of all application emojis matching asset directory...")

    # Get all potential emoji names from the asset directory
    emoji_names_to_delete: set[str] = set()
    try:
        for file_path in self.emojis_path.iterdir():
            if file_path.is_file() and _is_valid_emoji_name(file_path.stem):
                emoji_names_to_delete.add(file_path.stem)
    except OSError as e:
        logger.error(f"Failed to list files in emoji directory {self.emojis_path}: {e}")
        return [], []

    if not emoji_names_to_delete:
        logger.warning(f"No valid emoji names found in directory: {self.emojis_path}")
        return [], []

    deleted_names: list[str] = []
    failed_names: list[str] = []

    # Process each emoji name
    for emoji_name in emoji_names_to_delete:
        logger.debug(f"Attempting to delete emoji: '{emoji_name}'")

        if await self._delete_discord_emoji(emoji_name):
            deleted_names.append(emoji_name)
        else:
            failed_names.append(emoji_name)

    logger.info(
        f"Emoji deletion finished. Deleted: {len(deleted_names)}, Failed/Not Found: {len(failed_names)}.",
    )

    return deleted_names, failed_names

Functions

_is_valid_emoji_name(name: str) -> bool

Checks if an emoji name meets basic validity criteria.

Source code in tux/utils/emoji.py
Python
def _is_valid_emoji_name(name: str) -> bool:
    """Checks if an emoji name meets basic validity criteria."""
    return bool(name and len(name) >= MIN_EMOJI_NAME_LENGTH)

_find_emoji_file(base_path: Path, name: str) -> Path | None

Finds the local file corresponding to an emoji name within a base path.

Source code in tux/utils/emoji.py
Python
def _find_emoji_file(base_path: Path, name: str) -> Path | None:
    """Finds the local file corresponding to an emoji name within a base path."""
    if not _is_valid_emoji_name(name):
        logger.warning(f"Attempted to find file for invalid emoji name: '{name}'")
        return None

    for ext in VALID_EMOJI_EXTENSIONS:
        potential_path = base_path / f"{name}{ext}"

        if potential_path.is_file():
            logger.trace(f"Found local file for '{name}': {potential_path}")

            return potential_path

    logger.error(f"Cannot find local file for emoji '{name}' in {base_path}.")
    return None

_read_emoji_file(file_path: Path) -> bytes | None

Reads image bytes from a file path, handling errors.

Source code in tux/utils/emoji.py
Python
def _read_emoji_file(file_path: Path) -> bytes | None:
    """Reads image bytes from a file path, handling errors."""
    try:
        with file_path.open("rb") as f:
            img_bytes = f.read()
        logger.trace(f"Read {len(img_bytes)} bytes from {file_path}.")

        return img_bytes  # noqa: TRY300

    except OSError as e:
        logger.error(f"Failed to read local file '{file_path}': {e}")
        return None

    except Exception as e:
        logger.exception(f"An unexpected error occurred reading file '{file_path}': {e}")
        return None