# encoding: utf-8
__all__ = ['Context']
from typing import Any, List, Type, Optional, TypeVar, Union
import discord
import jishaku
import lifesaver
from discord.ext import commands
T = TypeVar('T')
[docs]class Context(commands.Context):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
#: The paginator associated with this context.
#:
#: To mimic :meth:`send`, the default prefix and suffix
#: is ``''``, and the ``max_size`` is 1,900 to prevent filling the chat
#: window when the paginator interface automatically kicks in
#: (see :meth:`paginate`).
self.paginator = commands.Paginator(prefix='', suffix='', max_size=1900)
[docs] def emoji(self, *args, **kwargs) -> Union[str, discord.Emoji]:
"""A shortcut to :meth:`lifesaver.bot.BotBase.emoji`."""
return self.bot.emoji(*args, **kwargs)
[docs] def tick(self, *args, **kwargs) -> Union[str, discord.Emoji]:
"""A shortcut to :meth:`lifesaver.bot.BotBase.tick`."""
return self.bot.tick(*args, **kwargs)
@property
def pool(self) -> 'asyncpg.pool.Pool':
"""A shortcut to :attr:`lifesaver.bot.BotBase.pool`."""
return self.bot.pool
@property
def can_send_embeds(self) -> bool:
"""Return whether the bot can send embeds in this context."""
if self.guild is None:
return True
perms = self.channel.permissions_for(self.guild.me)
return perms.embed_links
[docs] async def send(
self,
content: Any = None,
*args,
scrub: bool = True,
**kwargs
) -> discord.Message:
"""Send a message to this context. Identical to :meth:`discord.abc.Messageable.send`.
If ``scrub`` is ``True``, then @everyone and @here mentions are removed
from the content (after going through :py:func:`str`).
"""
if content is not None:
content = str(content)
if scrub:
content = content.replace('@everyone', '@\u200beveryone') \
.replace('@here', '@\u200bhere')
return await super().send(content, *args, **kwargs)
[docs] async def confirm(
self,
title: str,
message=discord.Embed.Empty,
*,
color: discord.Color = discord.Color.red(),
delete_after: bool = False,
cancellation_message: str = None
) -> bool:
"""Create a confirmation prompt for the user. Returns whether the user
reacted with an affirmative emoji.
Parameters
----------
title
The title of the confirmation prompt.
message
The message (description) of the confirmation prompt.
color
The color of the embed. Defaults to :meth:`discord.Color.red`.
delete_after
Deletes the confirmation after a choice has been picked.
cancellation_message
A message to send after cancelling.
Returns
-------
bool
Whether the user confirmed or not.
"""
embed = discord.Embed(title=title, description=message, color=color)
msg: discord.Message = await self.send(embed=embed)
reactions = [self.emoji('generic.yes'), self.emoji('generic.no')]
for emoji in reactions:
await msg.add_reaction(emoji)
def check(reaction: discord.Reaction, user: discord.User) -> bool:
return (
user == self.author
and reaction.message.id == msg.id
and reaction.emoji in reactions
)
reaction, _ = await self.bot.wait_for('reaction_add', check=check)
if delete_after:
await msg.delete()
confirmed = reaction.emoji == reactions[0]
if not confirmed and cancellation_message:
await self.send(cancellation_message)
return confirmed
[docs] async def wait_for_response(self) -> discord.Message:
"""Wait for a message from the message author, then returns it.
The message we are waiting for will only be accepted if it was sent by
the original command invoker, and if it was sent in the same channel as
the command message.
Returns
-------
discord.Message
The sent message.
"""
def check(msg: discord.Message):
if isinstance(msg.channel, discord.DMChannel):
# Accept any message, because we are in a DM.
return True
return msg.channel == self.channel and msg.author == self.author
return await self.bot.wait_for('message', check=check)
[docs] async def pick_from_list(
self,
choices: List[T],
*,
delete_after_choice: bool = False,
tries: int = 3
) -> Optional[T]:
"""Send a list of items, allowing the user to pick one. Returns the
picked item.
The choices are formatted with :func:`lifesaver.utils.formatting.format_list`.
Parameters
----------
choices
The list of choices.
delete_after_choice
Deletes the message prompt after the user has picked.
tries
The amount of tries to grant the user.
"""
choices_list = lifesaver.utils.format_list(choices)
choices_message = await self.send('Pick one, or send `cancel`.\n\n' + choices_list)
remaining_tries = tries
picked = None
while True:
if remaining_tries <= 0:
await self.send('You ran out of tries, I give up!')
return None
msg = await self.wait_for_response()
if msg.content == 'cancel':
await self.send('Canceled selection.')
break
try:
chosen_index = int(msg.content) - 1
except ValueError:
await self.send('Invalid number. Send the number of the item you want.')
remaining_tries -= 1
continue
if chosen_index < 0 or chosen_index > len(choices) - 1:
await self.send('Invalid choice. Send the number of the item you want.')
remaining_tries -= 1
else:
picked = choices[chosen_index]
if delete_after_choice:
await choices_message.delete()
await msg.delete()
break
return picked
[docs] def add_line(self, line) -> None:
"""Add a line to the paginator.
Works exactly like :meth:`discord.ext.commands.Paginator.add_line`.
See :attr:`paginator` and :meth:`paginate`.
"""
self.paginator.add_line(line)
[docs] async def send_pages(self) -> None:
"""Send the pages in the paginator.
You probably want to use :meth:`paginate` instead, as it automatically
wraps the pages in a :class:`jishaku.paginators.PaginatorInterface` if
there's more than one page.
"""
for page in self.paginator.pages:
await self.send(page)
[docs] async def paginate(
self,
*,
force_interface: bool = False,
interface: Type[jishaku.paginators.PaginatorInterface] = jishaku.paginators.PaginatorInterface,
) -> Optional[jishaku.paginators.PaginatorInterface]:
"""Send the pages in the paginator in an appropriate manner.
Adding to the paginator is done by :meth:`add_line` or manual access to
:attr:`paginator`.
If there's more than one page present (or ``force_interface`` is
``True``), then the paginator is wrapped in an :class:`jishaku.paginators.PaginatorInterface`,
sent, and returned. This is for maximum user convenience as it allows
them to browse the pages interactively using reaction buttons.
Otherwise, the only page is sent as a message without being wrapped.
Parameters
----------
force_interface
Forces the paginator to be sent through a :class:`jishaku.paginators.PaginatorInterface`.
interface
Customizes the paginator interface to use. Must be a subclass of
:class:`jishaku.paginators.PaginatorInterface`.
Raises
------
RuntimeError
The paginator is empty.
"""
if (
# We're using `_pages` here because the `pages` attribute closes the
# page if the current page is nonempty, which is not what we want.
not self.paginator._pages
# The prefix is always present as a line in the page, so if it's the
# only line in the page, then it's empty.
and len(self.paginator._current_page) == 1
):
raise RuntimeError('Cannot paginate with an empty paginator')
if not issubclass(interface, jishaku.paginators.PaginatorInterface):
raise TypeError(
f"Provided custom interface ({interface!r}) isn't a subclass of jishaku.paginators.PaginatorInterface")
if len(self.paginator._pages) > 1 or force_interface:
interface_instance = interface(self.bot, self.paginator, owner=self.author)
await interface_instance.send_to(self)
return interface_instance
else:
# Send the lone page normally.
await self.send_pages()
[docs] async def ok(self, emoji: str = None) -> None:
"""Respond with an emoji in acknowledgement to an action performed by the user.
This method tries to react to the original message, falling back to the
emoji being sent a message in the channel. This additionally falls back
to sending the author a direct message with the emoji.
If all of these fail, the message author will not be notified.
Parameters
----------
emoji
The emoji to react with.
"""
emoji = emoji or self.emoji('generic.ok')
actions = [self.message.add_reaction, self.send, self.author.send]
for action in actions:
try:
await action(emoji)
break
except discord.HTTPException:
pass