Source code for pylav.core.context

from __future__ import annotations

import contextlib
import datetime
import pathlib
from collections.abc import Iterable
from copy import copy
from typing import TYPE_CHECKING, Any

import discord
from discord import Attachment, Message, MessageType, PartialMessageable
from discord.ext import commands as dpy_command
from discord.ext.commands import Context as DpyContext
from discord.ext.commands.view import StringView
from discord.types.embed import EmbedType
from discord.utils import MISSING as D_MISSING  # noqa

from pylav.type_hints.bot import DISCORD_BOT_TYPE, DISCORD_COG_TYPE, DISCORD_CONTEXT_TYPE, DISCORD_INTERACTION_TYPE
from pylav.utils.vendor.redbot import MessagePredicate

try:
    from redbot.core.commands import Command
    from redbot.core.commands import Context as OriginalContextClass
except ImportError:
    from discord.ext.commands import Command
    from discord.ext.commands import Context as OriginalContextClass


try:
    from redbot.core.i18n import Translator

    _ = Translator("PyLav", pathlib.Path(__file__))
except ImportError:
    Translator = None

    def _(string: str) -> str:
        return string


if TYPE_CHECKING:
    from pylav.players.player import Player


[docs] class PyLavContext(OriginalContextClass): _original_ctx_or_interaction: DISCORD_CONTEXT_TYPE | DISCORD_INTERACTION_TYPE | None bot: DISCORD_BOT_TYPE client: DISCORD_BOT_TYPE interaction: DISCORD_INTERACTION_TYPE | None def __init__( self, *, message: Message, bot: DISCORD_BOT_TYPE, view: StringView, args: list[Any] = D_MISSING, kwargs: dict[str, Any] = D_MISSING, prefix: str | None = None, command: Command[Any, ..., Any] | None = None, invoked_with: str | None = None, invoked_parents: list[str] = D_MISSING, invoked_subcommand: Command[Any, ..., Any] | None = None, # noqa subcommand_passed: str | None = None, command_failed: bool = False, current_parameter: discord.ext.commands.Parameter | None = None, current_argument: str | None = None, interaction: DISCORD_INTERACTION_TYPE | None = None, ): super().__init__( message=message, bot=bot, view=view, args=args, kwargs=kwargs, prefix=prefix, command=command, invoked_with=invoked_with, invoked_parents=invoked_parents, invoked_subcommand=invoked_subcommand, subcommand_passed=subcommand_passed, command_failed=command_failed, current_parameter=current_parameter, current_argument=current_argument, interaction=interaction, ) self._original_ctx_or_interaction = None self.lavalink = bot.pylav self.pylav = bot.pylav @discord.utils.cached_property def author(self) -> discord.User | discord.Member: """Union[:class:`~discord.User`, :class:`.Member`]: Returns the author associated with this context's command. Shorthand for :attr:`.Message.author` """ # When using client.get_context() on a button interaction the "author" becomes the bot user # This ensures the original author remains the author of the context if isinstance(self._original_ctx_or_interaction, discord.Interaction): return self._original_ctx_or_interaction.user elif isinstance(self._original_ctx_or_interaction, DpyContext): return self._original_ctx_or_interaction.author else: return self.message.author @property def cog(self) -> DISCORD_COG_TYPE | None: """Optional[:class:`.Cog`]: Returns the cog associated with this context's command. None if it does not exist""" return None if self.command is None else self.command.cog @discord.utils.cached_property def guild(self) -> discord.Guild | None: """Optional[:class:`.Guild`]: Returns the guild associated with this context's command. None if not available""" return getattr(self.author, "guild", None) @discord.utils.cached_property def channel(self) -> discord.abc.MessageableChannel: """Union[:class:`.abc.Messageable`]: Returns the channel associated with this context's command. Shorthand for :attr:`.Message.channel`. """ if isinstance(self._original_ctx_or_interaction, (discord.Interaction, DpyContext)): return self._original_ctx_or_interaction.channel # type: ignore else: return self.message.channel @property def player(self) -> Player | None: """ Get player """ return self.pylav.get_player(self.guild)
[docs] async def connect_player( self, channel: discord.channel.VocalGuildChannel = None, self_deaf: bool | None = None ) -> Player: """ Connect player """ requester = self.author channel = channel or self.author.voice.channel return await self.pylav.connect_player(requester=requester, channel=channel, self_deaf=self_deaf)
@property def original_ctx_or_interaction(self) -> DISCORD_CONTEXT_TYPE | DISCORD_INTERACTION_TYPE | None: """ Get original ctx or interaction """ return self._original_ctx_or_interaction
[docs] async def construct_embed( self, *, embed: discord.Embed = None, colour: discord.Colour | int | None = None, color: discord.Colour | int | None = None, title: str = None, embed_type: EmbedType = "rich", url: str = None, description: str = None, timestamp: datetime.datetime = None, author_name: str = None, author_url: str = None, thumbnail: str = None, footer: str = None, footer_url: str = None, messageable: discord.abc.Messageable | DISCORD_INTERACTION_TYPE = None, ) -> discord.Embed: """ Construct embed """ return await self.pylav.construct_embed( embed=embed, colour=colour, color=color, title=title, embed_type=embed_type, url=url, description=description, timestamp=timestamp, author_name=author_name, author_url=author_url, thumbnail=thumbnail, footer=footer, footer_url=footer_url, messageable=messageable or self, )
[docs] @classmethod async def from_interaction(cls, interaction: DISCORD_INTERACTION_TYPE, /) -> PyLavContext: # When using this on a button interaction it raises an error as expected. # This makes the `get_context` method work with buttons by storing the original context added_dummy = False if isinstance(interaction, discord.Interaction) and interaction.command is None: setattr(interaction, "_cs_command", _dummy_command) added_dummy = True # Circular import from discord.ext.commands.bot import BotBase if not isinstance(interaction.client, BotBase): raise TypeError("Interaction client is not derived from commands.Bot or commands.AutoShardedBot") command = interaction.command if command is None: raise ValueError("interaction does not have command data") bot: DISCORD_BOT_TYPE = interaction.client # type: ignore data = interaction.data # type: ignore if interaction.message is None: synthetic_payload = { "id": interaction.id, "reactions": [], "embeds": [], "mention_everyone": False, "tts": False, "pinned": False, "edited_timestamp": None, "type": MessageType.chat_input_command if data.get("type", 1) == 1 else MessageType.context_menu_command, "flags": 64, "content": "", "mentions": [], "mention_roles": [], "attachments": [], } if interaction.channel_id is None: raise RuntimeError("interaction channel ID is null, this is probably a Discord bug") channel = interaction.channel or PartialMessageable( state=interaction._state, guild_id=interaction.guild_id, id=interaction.channel_id ) message = Message(state=interaction._state, channel=channel, data=synthetic_payload) # type: ignore message.author = interaction.user message.attachments = [a for _, a in interaction.namespace if isinstance(a, Attachment)] else: message = interaction.message prefix = "/" if data.get("type", 1) == 1 else "\u200b" # Mock the prefix ctx = cls( message=message, bot=bot, view=StringView(""), args=[], kwargs={}, prefix=prefix, interaction=interaction, invoked_with=command.name, command=command, # type: ignore # this will be a hybrid command, technically ) interaction._baton = ctx ctx.command_failed = interaction.command_failed if added_dummy: ctx.command = None ctx._original_ctx_or_interaction = interaction return ctx
[docs] def dispatch_command( self, message: discord.Message, command: Command, author: discord.abc.User, args: list[str], prefix: str = None ) -> None: """ Dispatch command """ command_str = f"{prefix}{command.qualified_name} {' '.join(args)}" msg = copy(message) msg.author = author msg.content = command_str self.bot.dispatch("message", msg)
[docs] async def send_interactive( self, messages: Iterable[str], box_lang: str = None, timeout: int = 15, embed: bool = False ) -> list[discord.Message]: """Send multiple messages interactively. The user will be prompted for whether or not they would like to view the next message, one at a time. They will also be notified of how many messages are remaining on each prompt. Parameters ---------- messages : `iterable` of `str` The messages to send. box_lang : str If specified, each message will be contained within a codeblock of this language. timeout : int How long the user has to respond to the prompt before it times out. After timing out, the bot deletes its prompt message. embed : bool Whether or not to send the messages as embeds. """ messages = tuple(messages) ret = [] for idx, page in enumerate(messages, 1): if box_lang is None: msg = ( await self.send(embed=await self.pylav.construct_embed(description=page, messageable=self)) if embed else await self.send(page) ) elif embed: msg = await self.send( embed=await self.pylav.construct_embed(description=f"```{box_lang}\n{page}\n```", messageable=self) ) else: msg = await self.send(f"```{box_lang}\n{page}\n```") ret.append(msg) n_remaining = len(messages) - idx if n_remaining > 0: match n_remaining: case 1: message = _("There is still 1 message remaining. Type `more` to continue.") case __: message = _( "There are still {remaining_variable_do_not_translate} messages remaining. Type `more` to continue." ).format(remaining_variable_do_not_translate=n_remaining) query = await self.send(message) try: resp = await self.bot.wait_for( "message", check=MessagePredicate.lower_equal_to("more", self), timeout=timeout, ) except TimeoutError: with contextlib.suppress(discord.HTTPException): await query.delete() break else: try: await self.channel.delete_messages((query, resp)) except (discord.HTTPException, AttributeError): # In case the bot can't delete other users' messages, # or is not a bot account # or channel is a DM with contextlib.suppress(discord.HTTPException): await query.delete() return ret
@dpy_command.command(name="__dummy_command", hidden=True, disabled=True) async def _dummy_command(self, context: PyLavContext) -> None: # noqa """Does nothing"""