You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Enso-Bot/cogs/help.py

718 lines
25 KiB
Python

# Help paginator by Rapptz
# Edited by F4stZ4p
import asyncio
import datetime
from typing import Optional
import discord
from discord import Embed, DMChannel
from discord.ext import commands
from discord.ext.commands import Cog, command
from settings import enso_embedmod_colours, hammyMention, enso_feedback_ID
class CannotPaginate(Exception):
pass
class Pages:
"""Implements a paginator that queries the user for the
pagination interface.
Pages are 1-index based, not 0-index based.
If the user does not reply within 2 minutes then the pagination
interface exits automatically.
Parameters
------------
ctx: Context
The context of the command.
entries: List[str]
A list of entries to paginate.
per_page: int
How many entries show up per page.
show_entry_count: bool
Whether to show an entry count in the footer.
Attributes
-----------
embed: discord.Embed
The embed object that is being used to send pagination info.
Feel free to modify this externally. Only the description,
footer fields, and colour are internally modified.
permissions: discord.Permissions
Our permissions for the channel.
"""
def __init__(self, ctx, *, entries, per_page=4, show_entry_count=True):
self.bot = ctx.bot
self.prefix = ctx.prefix
self.entries = entries
self.message = ctx.message
self.channel = ctx.channel
self.author = ctx.author
self.per_page = per_page
pages, left_over = divmod(len(self.entries), self.per_page)
if left_over:
pages += 1
self.maximum_pages = pages
self.embed = discord.Embed(colour=enso_embedmod_colours,
timestamp=datetime.datetime.utcnow())
self.paginating = len(entries) > per_page
self.show_entry_count = show_entry_count
self.reaction_emojis = [
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page),
('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page),
('\N{BLACK SQUARE FOR STOP}', self.stop_pages),
('\N{INFORMATION SOURCE}', self.show_help),
]
if ctx.guild is not None:
self.permissions = self.channel.permissions_for(ctx.guild.me)
else:
self.permissions = self.channel.permissions_for(ctx.bot.user)
if not self.permissions.embed_links:
raise CannotPaginate('Bot does not have Embed Links permission')
if not self.permissions.send_messages:
raise CannotPaginate('Bot Cannot Send Messages')
if self.paginating:
# verify we can actually use the pagination session
if not self.permissions.add_reactions:
raise CannotPaginate('Bot does not have Add Reactions permission')
if not self.permissions.read_message_history:
raise CannotPaginate('Bot does not have Read Message History permission')
def get_page(self, page):
base = (page - 1) * self.per_page
return self.entries[base:base + self.per_page]
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
p = []
for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)):
p.append(f'{index}. {entry}')
if self.maximum_pages > 1:
if self.show_entry_count:
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
else:
text = f'Page {page}/{self.maximum_pages}'
self.embed.set_footer(text=text)
if not self.paginating:
self.embed.description = '\n'.join(p)
return await self.channel.send(embed=self.embed)
if not first:
self.embed.description = '\n'.join(p)
await self.message.edit(embed=self.embed)
return
p.append('')
p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
self.embed.description = '\n'.join(p)
self.message = await self.channel.send(embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
await self.message.add_reaction(reaction)
async def checked_show_page(self, page):
if page != 0 and page <= self.maximum_pages:
await self.show_page(page)
async def first_page(self):
"""Show First Page"""
await self.show_page(1)
async def last_page(self):
"""Show Last Page"""
await self.show_page(self.maximum_pages)
async def next_page(self):
"""Show Next Page"""
await self.checked_show_page(self.current_page + 1)
async def previous_page(self):
"""Show Previous Page"""
await self.checked_show_page(self.current_page - 1)
async def show_current_page(self):
if self.paginating:
await self.show_page(self.current_page)
async def numbered_page(self):
"""Go to Given Page"""
to_delete = []
to_delete.append(await self.channel.send('What page do you want to go to?'))
def message_check(m):
return m.author == self.author and \
self.channel == m.channel and \
m.content.isdigit()
try:
msg = await self.bot.wait_for('message', check=message_check, timeout=30.0)
except asyncio.TimeoutError:
to_delete.append(await self.channel.send('Took too long.'))
await asyncio.sleep(5)
else:
page = int(msg.content)
to_delete.append(msg)
if page != 0 and page <= self.maximum_pages:
await self.show_page(page)
else:
to_delete.append(await self.channel.send(f'Invalid page given. ({page}/{self.maximum_pages})'))
await asyncio.sleep(5)
try:
await self.channel.delete_messages(to_delete)
except Exception:
pass
async def show_help(self):
"""shows this message"""
messages = ['Welcome to the interactive paginator!\n']
messages.append('This interactively allows you to see pages of text by navigating with '
'reactions. They are as follows:\n')
for (emoji, func) in self.reaction_emojis:
messages.append(f'{emoji} {func.__doc__}')
self.embed.description = '\n'.join(messages)
self.embed.clear_fields()
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(embed=self.embed)
async def go_back_to_current_page():
await asyncio.sleep(60.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())
async def stop_pages(self):
"""Deletes Help Message"""
await self.message.delete()
self.paginating = False
def react_check(self, reaction, user):
if user is None or user.id != self.author.id:
return False
if reaction.message.id != self.message.id:
return False
for (emoji, func) in self.reaction_emojis:
if reaction.emoji == emoji:
self.match = func
return True
return False
async def paginate(self):
"""Actually paginate the entries and run the interactive loop if necessary."""
first_page = self.show_page(1, first=True)
if not self.paginating:
await first_page
else:
# allow us to react to reactions right away if we're paginating
self.bot.loop.create_task(first_page)
while self.paginating:
try:
reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0)
except asyncio.TimeoutError:
self.paginating = False
try:
await self.message.clear_reactions()
except:
pass
finally:
break
try:
await self.message.remove_reaction(reaction, user)
except:
pass # can't remove it so don't bother doing so
await self.match()
class FieldPages(Pages):
"""Similar to Pages except entries should be a list of
tuples having (key, value) to show as embed fields instead.
"""
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
self.embed.clear_fields()
self.embed.description = discord.Embed.Empty
for key, value in entries:
self.embed.add_field(name=key, value=value, inline=False)
if self.maximum_pages > 1:
if self.show_entry_count:
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
else:
text = f'Page {page}/{self.maximum_pages}'
self.embed.set_footer(text=text)
if not self.paginating:
return await self.channel.send(embed=self.embed)
if not first:
await self.message.edit(embed=self.embed)
return
self.message = await self.channel.send(embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
await self.message.add_reaction(reaction)
import itertools
import inspect
import re
# ?help
# ?help Cog
# ?help command
# -> could be a subcommand
_mention = re.compile(r'<@!?([0-9]{1,19})>')
def cleanup_prefix(bot, prefix):
m = _mention.match(prefix)
if m:
user = bot.get_user(int(m.group(1)))
if user:
return f'@{user.name} '
return prefix
async def _can_run(cmd, ctx):
try:
return await cmd.can_run(ctx)
except:
return False
def _command_signature(cmd):
# this is modified from discord.py source
# which I wrote myself lmao
result = [cmd.qualified_name]
if cmd.usage:
result.append(cmd.usage)
return ' '.join(result)
params = cmd.clean_params
if not params:
return ' '.join(result)
for name, param in params.items():
if param.default is not param.empty:
# We don't want None or '' to trigger the [name=value] case and instead it should
# do [name] since [name=None] or [name=] are not exactly useful for the user.
should_print = param.default if isinstance(param.default, str) else param.default is not None
if should_print:
result.append(f'[{name}={param.default!r}]')
else:
result.append(f'`[{name}]`')
elif param.kind == param.VAR_POSITIONAL:
result.append(f'`[{name}...]`')
else:
result.append(f'`<{name}>`')
return ' '.join(result)
class HelpPaginator(Pages):
def __init__(self, ctx, entries, *, per_page=6):
super().__init__(ctx, entries=entries, per_page=per_page)
self.reaction_emojis.append(('\N{WHITE QUESTION MARK ORNAMENT}', self.show_bot_help))
self.total = len(entries)
@classmethod
async def from_cog(cls, ctx, cog):
cog_name = cog.__class__.__name__
if ctx.guild is None:
icon = ctx.author.avatar_url
else:
icon = ctx.guild.icon_url
# get the commands
entries = sorted(Cog.get_commands(cog), key=lambda c: c.name)
# remove the ones we can't run
entries = [cmd for cmd in entries if not cmd.hidden]
self = cls(ctx, entries)
self.title = f'(っ◔◡◔)っ {cog_name} (っ◔◡◔)っ'
self.embed.set_thumbnail(url=icon)
self.description = inspect.getdoc(cog)
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
return self
@classmethod
async def from_command(cls, ctx, command):
try:
entries = sorted(command.commands, key=lambda c: c.name)
except AttributeError:
entries = []
else:
entries = [cmd for cmd in entries if not cmd.hidden]
self = cls(ctx, entries)
if not isinstance(command, discord.ext.commands.Group):
if command.aliases:
aliases = " | ".join(command.aliases)
if command.usage:
self.title = f"{command.qualified_name} | {aliases} {command.signature}"
elif command.signature:
self.title = f"{command.qualified_name} | {aliases} `{command.signature}`"
else:
self.title = f"{command.qualified_name} | {aliases}"
else:
if command.usage:
self.title = f"{command.qualified_name} | {command.signature}"
elif command.signature:
self.title = f"{command.qualified_name} `{command.signature}`"
else:
self.title = f"{command.qualified_name}"
else:
if command.aliases:
aliases = " | ".join(command.aliases)
self.title = f"{command.name} | {aliases}"
else:
self.title = command.name
if command.description:
self.description = f'{command.description}\n\n{command.help}'
else:
self.description = command.help or 'No help given.'
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
return self
@classmethod
async def from_bot(cls, ctx):
def key(c):
return c.cog_name or '\u200bMisc'
entries = sorted(ctx.bot.commands, key=key)
nested_pages = []
per_page = 4
# 0: (cog, desc, commands) (max len == 9)
# 1: (cog, desc, commands) (max len == 9)
# ...
for cog, commands in itertools.groupby(entries, key=key):
plausible = [cmd for cmd in commands if not cmd.hidden]
if len(plausible) == 0:
continue
description = ctx.bot.get_cog(cog)
if description is None:
description = discord.Embed.Empty
else:
description = inspect.getdoc(description) or discord.Embed.Empty
nested_pages.extend(
(cog, description, plausible[i:i + per_page]) for i in range(0, len(plausible), per_page))
if ctx.guild is None:
icon = ctx.author.avatar_url
else:
icon = ctx.guild.icon_url
self = cls(ctx, nested_pages, per_page=1) # this forces the pagination session
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
self.embed.set_thumbnail(url=icon)
# swap the get_page implementation with one that supports our style of pagination
self.get_page = self.get_bot_page
self._is_bot = True
# replace the actual total
self.total = sum(len(o) for _, _, o in nested_pages)
return self
def get_bot_page(self, page):
cog, description, commands = self.entries[page - 1]
self.title = f'{cog} Commands'
self.description = description
return commands
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
self.embed.clear_fields()
self.embed.description = self.description
self.embed.title = self.title
self.embed.set_footer(text=f'"{self.prefix}help command | module" For More Information!')
signature = _command_signature
for entry in entries:
self.embed.add_field(name=signature(entry), value=entry.short_doc or "No help given", inline=False)
if self.maximum_pages:
self.embed.set_author(name=f'Page {page}/{self.maximum_pages} ({self.total} commands)')
if not self.paginating:
return await self.channel.send(embed=self.embed)
if not first:
await self.message.edit(embed=self.embed)
return
self.message = await self.channel.send(embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
await self.message.add_reaction(reaction)
async def show_help(self):
"""Shows This Message"""
self.embed.title = 'Paginator help'
self.embed.description = 'Hello! Welcome to the help page.'
messages = [f'{emoji} {func.__doc__}' for emoji, func in self.reaction_emojis]
self.embed.clear_fields()
self.embed.add_field(name='What are these reactions for?', value='\n'.join(messages), inline=False)
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(embed=self.embed)
async def go_back_to_current_page():
await asyncio.sleep(30.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())
async def show_bot_help(self):
"""Information On The Bot"""
self.embed.title = 'Using Ensō~Chan'
self.embed.description = 'Hiya! This is the Help Page!'
self.embed.clear_fields()
entries = (
('`<argument>`', 'This means the argument is **required**.'),
('`[argument]`', 'This means the argument is **optional**.'),
('`[A|B]`', 'This means the it can be **either A or B**.'),
('`[argument...]`', 'This means you can have multiple arguments.\n' \
'Now that you know the basics, it should be noted that...\n' \
'**You do not type in the brackets!**')
)
self.embed.add_field(name='How do I use Ensō~Chan', value='Reading the signature is pretty straightforward')
for name, value in entries:
self.embed.add_field(name=name, value=value, inline=False)
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(embed=self.embed)
async def go_back_to_current_page():
await asyncio.sleep(30.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())
def send_feedback(message, author):
"""Preparing Embed to send to the support server"""
embed = Embed(title="Feedback!",
colour=enso_embedmod_colours,
timestamp=datetime.datetime.utcnow())
embed.set_thumbnail(url=author.avatar_url)
embed.set_footer(text=f"Send By {author}")
fields = [("Member", author.mention, False),
("Message", message.content, False)]
for name, value, inline in fields:
embed.add_field(name=name, value=value, inline=inline)
return embed
def message_sent_confirmation():
"""Preparing Embed to be sent to the user after the message has been received successfully"""
ConfirmationEmbed = Embed(title="Thank you for your feedback!",
description=f"**Message relayed to {hammyMention}**",
colour=enso_embedmod_colours,
timestamp=datetime.datetime.utcnow())
ConfirmationEmbed.set_footer(text=f"Thanks Once Again! ~ Hammy")
return ConfirmationEmbed
def error_handling(author):
"""Preparing embed to send if the message is not suitable"""
ErrorHandlingEmbed = Embed(
title="Uh Oh! Please make sure the message is below **1024** characters!",
colour=enso_embedmod_colours,
timestamp=datetime.datetime.utcnow())
ErrorHandlingEmbed.set_footer(text=f"Sent To {author}")
return ErrorHandlingEmbed
# Set up the Cog
class Help(Cog):
"""Help Commands!"""
def __init__(self, bot):
self.bot = bot
@command(name='help', aliases=["Help"])
async def _help(self, ctx, *, cmd: Optional[str] = None):
"""Shows help about a command or the bot"""
try:
if cmd is None:
p = await HelpPaginator.from_bot(ctx)
else:
entity = ctx.bot.get_cog(cmd) or ctx.bot.get_command(cmd)
if entity is None:
clean = cmd.replace('@', '@\u200b')
return await ctx.send(f"**Command or Category '{clean}' Not Found.**")
elif isinstance(entity, commands.Command):
p = await HelpPaginator.from_command(ctx, entity)
else:
p = await HelpPaginator.from_cog(ctx, entity)
await p.paginate()
except Exception as ex:
await ctx.send(f"**{ex}**")
@command(name="support", aliases=["Support"])
async def support(self, ctx):
"""Joining Support Server And Sending Feedback"""
embed = Embed(title="Support Server!",
description=f"Do **{ctx.prefix}feedback** to send me feedback about the bot and/or report any issues "
f"that you are having!",
url="https://discord.gg/SZ5nexg",
colour=enso_embedmod_colours,
timestamp=datetime.datetime.utcnow())
embed.set_thumbnail(url=ctx.bot.user.avatar_url)
embed.set_footer(text=f"Requested by {ctx.author}", icon_url='{}'.format(ctx.author.avatar_url))
fields = [("Developer", hammyMention, False),
("Data Collection",
"\nData Stored:" +
"\n- User ID" +
"\n- Guild ID" +
"\n\n If you wish to delete this data being stored about you. Follow steps outlined below:" +
"\n\n1) **Enable Developer Mode**" +
"\n2) Note down your **User ID** and the **Guild ID** (You must have left this guild or are planning to leave)" +
f"\n3) Join support server and notify me or use **{ctx.prefix}feedback** to notify me", False)]
# Add fields to the embed
for name, value, inline in fields:
embed.add_field(name=name, value=value, inline=inline)
await ctx.send(embed=embed)
@command(name="feedback", aliases=["Feedback"])
async def feedback(self, ctx):
"""Sending Feedback to Support Server"""
# Get the #feedback channel within the support server
channel = self.bot.get_channel(enso_feedback_ID)
embed = Embed(title="Provide Feedback!",
description=f"Hiya! Please respond to this message with the feedback you want to provide!"
f"\n(You have **5 minutes** to respond. Make sure it is a **single message** and under **1024** characters!)",
url="https://discord.gg/SZ5nexg",
colour=enso_embedmod_colours,
timestamp=datetime.datetime.utcnow())
embed.set_footer(text=f"Requested by {ctx.author}", icon_url=ctx.author.avatar_url)
helper = await ctx.author.send(embed=embed)
def check(m):
"""Ensure that the feedback received is from the author's DMs"""
return m.author == ctx.author and isinstance(m.channel, DMChannel)
try:
# Wait for feedback from author
msg = await ctx.bot.wait_for('message', check=check, timeout=300.0)
# Make sure sure that the message is below 1024 characters
while len(msg.content) > 1024 and check(msg):
await ctx.author.send(embed=error_handling(ctx.author))
# Wait for feedback again
msg = await ctx.bot.wait_for('message', check=check, timeout=300.0)
# Once message is below 1024 characters
# Send confirmation message to author to let them know feedback has been sent
# Send the feedback entered from the author into support server
if len(msg.content) < 1024 and check(msg):
await ctx.author.send(embed=message_sent_confirmation())
await channel.send(embed=send_feedback(msg, ctx.author))
# Edit current embed to show error that the feedback timed out
except asyncio.TimeoutError:
embed = Embed(title="(。T ω T。) You waited too long",
description=f"Do **{ctx.prefix}feedback** to try again!",
colour=enso_embedmod_colours,
timestamp=datetime.datetime.utcnow())
embed.set_footer(text=f"Sent To {ctx.author}", icon_url=ctx.author.avatar_url)
# Send out an error message if the user waited too long
await helper.edit(embed=embed)
def setup(bot):
bot.add_cog(Help(bot))