feat(command|prefix): add prefix commands (new, del, list)

known issue: `prefix list` return weird result when there is no custom prefix
This commit is contained in:
Romain J 2019-12-17 22:41:54 +01:00
parent d9427d1863
commit 97980e96d1
12 changed files with 1138 additions and 67 deletions

11
bot.py
View file

@ -28,6 +28,7 @@ l_extensions: List[str] = [
'cogs.utility',
'cogs.logs',
'cogs.poll',
'cogs.help',
'jishaku',
]
@ -38,8 +39,9 @@ async def _prefix_callable(bot, message: discord.message) -> list:
extras = []
if str(message.guild.id) in bot.prefixes:
extras.extend(
bot.prefixes.get(str(message.guild.id), "prefixes")
.split('-sep-')
bot.prefixes.get(str(message.guild.id), "prefixes").split(
bot.config.get("misc", "separator")
)
)
return commands.when_mentioned_or(*extras)(bot, message)
@ -67,7 +69,7 @@ class TuxBot(commands.AutoShardedBot):
self.prefixes = Config('./configs/prefixes.cfg')
self.blacklist = Config('./configs/blacklist.cfg')
self.version = Version(10, 1, 0, pre_release='a0', build=build)
self.version = Version(10, 1, 0, pre_release='a5', build=build)
for extension in l_extensions:
try:
@ -83,7 +85,8 @@ class TuxBot(commands.AutoShardedBot):
+ extension, exc_info=e)
async def is_owner(self, user: discord.User) -> bool:
return str(user.id) in self.config.get("permissions", "owners").split(',')
return str(user.id) in self.config.get("permissions", "owners").split(
',')
async def on_socket_response(self, msg):
self._prev_events.append(msg)

View file

@ -10,6 +10,7 @@ from discord.ext import commands
from bot import TuxBot
from .utils.lang import Texts
from .utils.extra import commandExtra, groupExtra
from .utils.models import Warn, Lang
log = logging.getLogger(__name__)
@ -61,9 +62,10 @@ class Admin(commands.Cog):
return e
"""---------------------------------------------------------------------"""
###########################################################################
@commands.group(name='say', invoke_without_command=True)
@groupExtra(name='say', invoke_without_command=True, category='admin',
description=Texts('commands').get('admin._say'))
async def _say(self, ctx: commands.Context, *, content: str):
if ctx.invoked_subcommand is None:
try:
@ -73,7 +75,8 @@ class Admin(commands.Cog):
await ctx.send(content)
@_say.command(name='edit')
@_say.command(name='edit',
description=Texts('commands').get('admin._say_edit'))
async def _say_edit(self, ctx: commands.Context, message_id: int, *,
content: str):
try:
@ -90,7 +93,8 @@ class Admin(commands.Cog):
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5)
@_say.command(name='to')
@_say.command(name='to',
description=Texts('commands').get('admin._say_to'))
async def _say_to(self, ctx: commands.Context,
channel: Union[discord.TextChannel, discord.User], *,
content):
@ -101,9 +105,10 @@ class Admin(commands.Cog):
await channel.send(content)
"""---------------------------------------------------------------------"""
###########################################################################
@commands.command(name='ban')
@commandExtra(name='ban', category='admin',
description=Texts('commands').get('admin._ban'))
async def _ban(self, ctx: commands.Context, user: discord.Member, *,
reason=""):
try:
@ -128,9 +133,10 @@ class Admin(commands.Cog):
Texts('utils', ctx).get("Unable to find the user..."),
delete_after=5)
"""---------------------------------------------------------------------"""
###########################################################################
@commands.command(name='kick')
@commandExtra(name='kick', category='admin',
description=Texts('commands').get('admin._kick'))
async def _kick(self, ctx: commands.Context, user: discord.Member, *,
reason=""):
try:
@ -155,9 +161,10 @@ class Admin(commands.Cog):
Texts('utils', ctx).get("Unable to find the user..."),
delete_after=5)
"""---------------------------------------------------------------------"""
###########################################################################
@commands.command(name='clear')
@commandExtra(name='clear', category='admin',
description=Texts('commands').get('admin._clear'))
async def _clear(self, ctx: commands.Context, count: int):
try:
await ctx.message.delete()
@ -165,14 +172,16 @@ class Admin(commands.Cog):
except discord.errors.Forbidden:
pass
"""---------------------------------------------------------------------"""
###########################################################################
@commands.group(name='react')
@groupExtra(name='react', category='admin',
description=Texts('commands').get('admin._react'))
async def _react(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
return
await ctx.send_help('react')
@_react.command(name='add')
@_react.command(name='add',
description=Texts('commands').get('admin._react_add'))
async def _react_add(self, ctx: commands.Context, message_id: int, *,
emojis: str):
emojis: list = emojis.split(' ')
@ -188,7 +197,8 @@ class Admin(commands.Cog):
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5)
@_react.command(name='clear')
@_react.command(name='clear',
description=Texts('commands').get('admin._react_remove'))
async def _react_remove(self, ctx: commands.Context, message_id: int):
try:
message: discord.Message = await ctx.channel.fetch_message(
@ -199,9 +209,11 @@ class Admin(commands.Cog):
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5)
"""---------------------------------------------------------------------"""
###########################################################################
@commands.group(name='delete', invoke_without_command=True)
@groupExtra(name='delete', invoke_without_command=True,
category='admin',
description=Texts('commands').get('admin._delete'))
async def _delete(self, ctx: commands.Context, message_id: int):
try:
await ctx.message.delete()
@ -217,7 +229,8 @@ class Admin(commands.Cog):
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5)
@_delete.command(name='from', aliases=['to', 'in'])
@_delete.command(name='from', aliases=['to', 'in'],
description=Texts('commands').get('admin._delete_from'))
async def _delete_from(self, ctx: commands.Context,
channel: discord.TextChannel, message_id: int):
try:
@ -234,7 +247,7 @@ class Admin(commands.Cog):
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5)
"""---------------------------------------------------------------------"""
###########################################################################
async def get_warn(self, ctx: commands.Context,
member: discord.Member = False):
@ -279,7 +292,8 @@ class Admin(commands.Cog):
self.bot.database.session.add(warn)
self.bot.database.session.commit()
@commands.group(name='warn', aliases=['warns'])
@groupExtra(name='warn', aliases=['warns'], category='admin',
description=Texts('commands').get('admin._warn'))
async def _warn(self, ctx: commands.Context):
await ctx.trigger_typing()
if ctx.invoked_subcommand is None:
@ -291,7 +305,8 @@ class Admin(commands.Cog):
await ctx.send(embed=e)
@_warn.command(name='add', aliases=['new'])
@_warn.command(name='add', aliases=['new'],
description=Texts('commands').get('admin._warn_new'))
async def _warn_new(self, ctx: commands.Context, member: discord.Member,
*, reason="N/A"):
member = await ctx.guild.fetch_member(member.id)
@ -370,7 +385,8 @@ class Admin(commands.Cog):
f"\n**{Texts('admin', ctx).get('Reason')}:** `{reason}`"
)
@_warn.command(name='remove', aliases=['revoke'])
@_warn.command(name='remove', aliases=['revoke'],
description=Texts('commands').get('admin._warn_remove'))
async def _warn_remove(self, ctx: commands.Context, warn_id: int):
warn = self.bot.database.session \
.query(Warn) \
@ -382,7 +398,8 @@ class Admin(commands.Cog):
await ctx.send(f"{Texts('admin', ctx).get('Warn with id')} `{warn_id}`"
f" {Texts('admin', ctx).get('successfully removed')}")
@_warn.command(name='show', aliases=['list'])
@_warn.command(name='show', aliases=['list'],
description=Texts('commands').get('admin._warn_show'))
async def _warn_show(self, ctx: commands.Context, member: discord.Member):
warns_list, warns = await self.get_warn(ctx, member)
@ -393,7 +410,8 @@ class Admin(commands.Cog):
await ctx.send(embed=e)
@_warn.command(name='edit', aliases=['change'])
@_warn.command(name='edit', aliases=['change'],
description=Texts('commands').get('admin._warn_edit'))
async def _warn_edit(self, ctx: commands.Context, warn_id: int, *, reason):
warn = self.bot.database.session \
.query(Warn) \
@ -406,9 +424,11 @@ class Admin(commands.Cog):
await ctx.send(f"{Texts('admin', ctx).get('Warn with id')} `{warn_id}`"
f" {Texts('admin', ctx).get('successfully edited')}")
"""---------------------------------------------------------------------"""
###########################################################################
@commands.command(name='language', aliases=['lang', 'langue', 'langage'])
@commandExtra(name='language', aliases=['lang', 'langue', 'langage'],
category='admin',
description=Texts('commands').get('admin._language'))
async def _language(self, ctx: commands.Context, locale: str):
available = self.bot.database.session \
.query(Lang.value) \
@ -436,6 +456,103 @@ class Admin(commands.Cog):
await ctx.send(
Texts('admin', ctx).get('Language changed successfully'))
###########################################################################
@groupExtra(name='prefix', aliases=['prefixes'], category='admin',
description=Texts('commands').get('admin._prefix'))
async def _prefix(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
await ctx.send_help('prefix')
@_prefix.command(name='add', aliases=['set', 'new'],
description=Texts('commands').get('admin._prefix_add'))
async def _prefix_add(self, ctx: commands.Context, prefix: str):
if str(ctx.guild.id) in self.bot.prefixes:
prefixes = self.bot.prefixes.get(
str(ctx.guild.id), "prefixes"
).split(
self.bot.config.get("misc", "separator")
)
if prefix in prefixes:
return await ctx.send(
Texts('admin', ctx).get('This prefix already exists')
)
else:
prefixes.append(prefix)
self.bot.prefixes.set(
str(ctx.guild.id),
"prefixes",
self.bot.config.get("misc", "separator")
.join(prefixes)
)
with open('./configs/prefixes.cfg', 'w') as configfile:
self.bot.prefixes.write(configfile)
else:
self.bot.prefixes.add_section(str(ctx.guild.id))
self.bot.prefixes.set(str(ctx.guild.id), "prefixes", prefix)
with open('./configs/prefixes.cfg', 'w') as configfile:
self.bot.prefixes.write(configfile)
await ctx.send(
Texts('admin', ctx).get('Prefix added successfully')
)
@_prefix.command(name='remove', aliases=['drop', 'del', 'delete'],
description=Texts('commands').get('admin._prefix_remove'))
async def _prefix_remove(self, ctx: commands.Context, prefix: str):
if str(ctx.guild.id) in self.bot.prefixes:
prefixes = self.bot.prefixes.get(
str(ctx.guild.id), "prefixes"
).split(
self.bot.config.get("misc", "separator")
)
if prefix in prefixes:
prefixes.remove(prefix)
self.bot.prefixes.set(
str(ctx.guild.id),
"prefixes",
self.bot.config.get("misc", "separator")
.join(prefixes)
)
with open('./configs/prefixes.cfg', 'w') as configfile:
self.bot.prefixes.write(configfile)
return await ctx.send(
Texts('admin', ctx).get('Prefix removed successfully')
)
await ctx.send(
Texts('admin', ctx).get('This prefix does not exist')
)
@_prefix.command(name='list', aliases=['show', 'all'],
description=Texts('commands').get('admin._prefix_list'))
async def _prefix_list(self, ctx: commands.Context):
extras = ['.']
if ctx.message.guild is not None:
extras = []
if str(ctx.message.guild.id) in self.bot.prefixes:
extras.extend(
self.bot.prefixes.get(str(ctx.message.guild.id),
"prefixes").split(
self.bot.config.get("misc", "separator")
)
)
prefixes = [self.bot.user.mention]
prefixes.extend(extras)
if len(prefixes) <= 1:
text = Texts('admin', ctx)\
.get('The only prefix for this guild is :\n')
else:
text = Texts('admin', ctx)\
.get('Available prefixes for this guild are :\n')
await ctx.send(text + "\n".join(prefixes))
def setup(bot: TuxBot):
bot.add_cog(Admin(bot))

View file

@ -25,10 +25,9 @@ class Basics(commands.Cog):
return os.popen(cmd).read().strip()
"""---------------------------------------------------------------------"""
###########################################################################
@commandExtra(name='ping',
category='basics',
@commandExtra(name='ping', category='basics',
description=Texts('commands').get('basics._ping'))
async def _ping(self, ctx: commands.Context):
start = time.perf_counter()
@ -45,7 +44,7 @@ class Basics(commands.Cog):
e.add_field(name='discordapp.com', value=f'{discordapp}ms')
await ctx.send(embed=e)
"""---------------------------------------------------------------------"""
###########################################################################
@staticmethod
def fetch_info():
@ -67,8 +66,7 @@ class Basics(commands.Cog):
return total, file_amount
@commandExtra(name='info', aliases=['about'],
category='basics',
@commandExtra(name='info', aliases=['about'], category='basics',
description=Texts('commands').get('basics._info'))
async def _info(self, ctx: commands.Context):
proc = psutil.Process()
@ -148,7 +146,7 @@ class Basics(commands.Cog):
await ctx.send(embed=e)
"""---------------------------------------------------------------------"""
###########################################################################
@commandExtra(name='credits', aliases=['contributors', 'authors'],
category='basics',

621
cogs/help.py Normal file
View file

@ -0,0 +1,621 @@
from discord.ext import commands
from .utils import checks, formats, time
from .utils.paginator import Pages
import discord
from collections import OrderedDict, deque, Counter
import os, datetime
import asyncio
import copy
import unicodedata
import inspect
import itertools
from typing import Union
class Prefix(commands.Converter):
async def convert(self, ctx, argument):
user_id = ctx.bot.user.id
if argument.startswith((f'<@{user_id}>', f'<@!{user_id}>')):
raise commands.BadArgument('That is a reserved prefix already in use.')
return argument
class FetchedUser(commands.Converter):
async def convert(self, ctx, argument):
if not argument.isdigit():
raise commands.BadArgument('Not a valid user ID.')
try:
return await ctx.bot.fetch_user(argument)
except discord.NotFound:
raise commands.BadArgument('User not found.') from None
except discord.HTTPException:
raise commands.BadArgument('An error occurred while fetching the user.') from None
class HelpPaginator(Pages):
def __init__(self, help_command, ctx, entries, *, per_page=4):
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)
self.help_command = help_command
self.prefix = help_command.clean_prefix
self.is_bot = False
def get_bot_page(self, page):
cog, description, commands = self.entries[page - 1]
self.title = f'{cog} Commands'
self.description = description
return commands
def prepare_embed(self, entries, page, *, first=False):
self.embed.clear_fields()
self.embed.description = self.description
self.embed.title = self.title
if self.is_bot:
value ='For more help, join the official bot support server: https://discord.gg/DWEaqMy'
self.embed.add_field(name='Support', value=value, inline=False)
self.embed.set_footer(text=f'Use "{self.prefix}help command" for more info on a command.')
for entry in entries:
signature = f'{entry.qualified_name} {entry.signature}'
self.embed.add_field(name=signature, 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)')
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):
"""shows how to use the bot"""
self.embed.title = 'Using the bot'
self.embed.description = 'Hello! Welcome to 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 this bot?', value='Reading the bot signature is pretty simple.')
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())
class PaginatedHelpCommand(commands.HelpCommand):
def __init__(self):
super().__init__(command_attrs={
'cooldown': commands.Cooldown(1, 3.0, commands.BucketType.member),
'help': 'Shows help about the bot, a command, or a category'
})
async def on_help_command_error(self, ctx, error):
if isinstance(error, commands.CommandInvokeError):
await ctx.send(str(error.original))
def get_command_signature(self, command):
parent = command.full_parent_name
if len(command.aliases) > 0:
aliases = '|'.join(command.aliases)
fmt = f'[{command.name}|{aliases}]'
if parent:
fmt = f'{parent} {fmt}'
alias = fmt
else:
alias = command.name if not parent else f'{parent} {command.name}'
return f'{alias} {command.signature}'
async def send_bot_help(self, mapping):
def key(c):
return c.cog_name or '\u200bNo Category'
bot = self.context.bot
entries = await self.filter_commands(bot.commands, sort=True, key=key)
nested_pages = []
per_page = 9
total = 0
for cog, commands in itertools.groupby(entries, key=key):
commands = sorted(commands, key=lambda c: c.name)
if len(commands) == 0:
continue
total += len(commands)
actual_cog = bot.get_cog(cog)
# get the description if it exists (and the cog is valid) or return Empty embed.
description = (actual_cog and actual_cog.description) or discord.Embed.Empty
nested_pages.extend((cog, description, commands[i:i + per_page]) for i in range(0, len(commands), per_page))
# a value of 1 forces the pagination session
pages = HelpPaginator(self, self.context, nested_pages, per_page=1)
# swap the get_page implementation to work with our nested pages.
pages.get_page = pages.get_bot_page
pages.is_bot = True
pages.total = total
await self.context.release()
await pages.paginate()
async def send_cog_help(self, cog):
entries = await self.filter_commands(cog.get_commands(), sort=True)
pages = HelpPaginator(self, self.context, entries)
pages.title = f'{cog.qualified_name} Commands'
pages.description = cog.description
await self.context.release()
await pages.paginate()
def common_command_formatting(self, page_or_embed, command):
page_or_embed.title = self.get_command_signature(command)
if command.description:
page_or_embed.description = f'{command.description}\n\n{command.help}'
else:
page_or_embed.description = command.help or 'No help found...'
async def send_command_help(self, command):
# No pagination necessary for a single command.
embed = discord.Embed(colour=discord.Colour.blurple())
self.common_command_formatting(embed, command)
await self.context.send(embed=embed)
async def send_group_help(self, group):
subcommands = group.commands
if len(subcommands) == 0:
return await self.send_command_help(group)
entries = await self.filter_commands(subcommands, sort=True)
pages = HelpPaginator(self, self.context, entries)
self.common_command_formatting(pages, group)
await self.context.release()
await pages.paginate()
class Meta(commands.Cog):
"""Commands for utilities related to Discord or the Bot itself."""
def __init__(self, bot):
self.bot = bot
self.old_help_command = bot.help_command
bot.help_command = PaginatedHelpCommand()
bot.help_command.cog = self
def cog_unload(self):
self.bot.help_command = self.old_help_command
async def cog_command_error(self, ctx, error):
if isinstance(error, commands.BadArgument):
await ctx.send(error)
@commands.command(hidden=True)
async def hello(self, ctx):
"""Displays my intro message."""
await ctx.send('Hello! I\'m a robot! Danny#0007 made me.')
@commands.command()
async def charinfo(self, ctx, *, characters: str):
"""Shows you information about a number of characters.
Only up to 25 characters at a time.
"""
def to_string(c):
digit = f'{ord(c):x}'
name = unicodedata.name(c, 'Name not found.')
return f'`\\U{digit:>08}`: {name} - {c} \N{EM DASH} <http://www.fileformat.info/info/unicode/char/{digit}>'
msg = '\n'.join(map(to_string, characters))
if len(msg) > 2000:
return await ctx.send('Output too long to display.')
await ctx.send(msg)
@commands.group(name='prefix', invoke_without_command=True)
async def prefix(self, ctx):
"""Manages the server's custom prefixes.
If called without a subcommand, this will list the currently set
prefixes.
"""
prefixes = self.bot.get_guild_prefixes(ctx.guild)
# we want to remove prefix #2, because it's the 2nd form of the mention
# and to the end user, this would end up making them confused why the
# mention is there twice
del prefixes[1]
e = discord.Embed(title='Prefixes', colour=discord.Colour.blurple())
e.set_footer(text=f'{len(prefixes)} prefixes')
e.description = '\n'.join(f'{index}. {elem}' for index, elem in enumerate(prefixes, 1))
await ctx.send(embed=e)
@prefix.command(name='add', ignore_extra=False)
@checks.is_mod()
async def prefix_add(self, ctx, prefix: Prefix):
"""Appends a prefix to the list of custom prefixes.
Previously set prefixes are not overridden.
To have a word prefix, you should quote it and end it with
a space, e.g. "hello " to set the prefix to "hello ". This
is because Discord removes spaces when sending messages so
the spaces are not preserved.
Multi-word prefixes must be quoted also.
You must have Manage Server permission to use this command.
"""
current_prefixes = self.bot.get_raw_guild_prefixes(ctx.guild.id)
current_prefixes.append(prefix)
try:
await self.bot.set_guild_prefixes(ctx.guild, current_prefixes)
except Exception as e:
await ctx.send(f'{ctx.tick(False)} {e}')
else:
await ctx.send(ctx.tick(True))
@prefix_add.error
async def prefix_add_error(self, ctx, error):
if isinstance(error, commands.TooManyArguments):
await ctx.send("You've given too many prefixes. Either quote it or only do it one by one.")
@prefix.command(name='remove', aliases=['delete'], ignore_extra=False)
@checks.is_mod()
async def prefix_remove(self, ctx, prefix: Prefix):
"""Removes a prefix from the list of custom prefixes.
This is the inverse of the 'prefix add' command. You can
use this to remove prefixes from the default set as well.
You must have Manage Server permission to use this command.
"""
current_prefixes = self.bot.get_raw_guild_prefixes(ctx.guild.id)
try:
current_prefixes.remove(prefix)
except ValueError:
return await ctx.send('I do not have this prefix registered.')
try:
await self.bot.set_guild_prefixes(ctx.guild, current_prefixes)
except Exception as e:
await ctx.send(f'{ctx.tick(False)} {e}')
else:
await ctx.send(ctx.tick(True))
@prefix.command(name='clear')
@checks.is_mod()
async def prefix_clear(self, ctx):
"""Removes all custom prefixes.
After this, the bot will listen to only mention prefixes.
You must have Manage Server permission to use this command.
"""
await self.bot.set_guild_prefixes(ctx.guild, [])
await ctx.send(ctx.tick(True))
@commands.command()
async def source(self, ctx, *, command: str = None):
"""Displays my full source code or for a specific command.
To display the source code of a subcommand you can separate it by
periods, e.g. tag.create for the create subcommand of the tag command
or by spaces.
"""
source_url = 'https://github.com/Rapptz/RoboDanny'
branch = 'rewrite'
if command is None:
return await ctx.send(source_url)
if command == 'help':
src = type(self.bot.help_command)
module = src.__module__
filename = inspect.getsourcefile(src)
else:
obj = self.bot.get_command(command.replace('.', ' '))
if obj is None:
return await ctx.send('Could not find command.')
# since we found the command we're looking for, presumably anyway, let's
# try to access the code itself
src = obj.callback.__code__
module = obj.callback.__module__
filename = src.co_filename
lines, firstlineno = inspect.getsourcelines(src)
if not module.startswith('discord'):
# not a built-in command
location = os.path.relpath(filename).replace('\\', '/')
else:
location = module.replace('.', '/') + '.py'
source_url = 'https://github.com/Rapptz/discord.py'
branch = 'master'
final_url = f'<{source_url}/blob/{branch}/{location}#L{firstlineno}-L{firstlineno + len(lines) - 1}>'
await ctx.send(final_url)
@commands.command(name='quit', hidden=True)
@commands.is_owner()
async def _quit(self, ctx):
"""Quits the bot."""
await self.bot.logout()
@commands.command()
async def avatar(self, ctx, *, user: Union[discord.Member, FetchedUser] = None):
"""Shows a user's enlarged avatar (if possible)."""
embed = discord.Embed()
user = user or ctx.author
avatar = user.avatar_url_as(static_format='png')
embed.set_author(name=str(user), url=avatar)
embed.set_image(url=avatar)
await ctx.send(embed=embed)
@commands.command()
async def info(self, ctx, *, user: Union[discord.Member, FetchedUser] = None):
"""Shows info about a user."""
user = user or ctx.author
if ctx.guild and isinstance(user, discord.User):
user = ctx.guild.get_member(user.id) or user
e = discord.Embed()
roles = [role.name.replace('@', '@\u200b') for role in getattr(user, 'roles', [])]
shared = sum(g.get_member(user.id) is not None for g in self.bot.guilds)
e.set_author(name=str(user))
def format_date(dt):
if dt is None:
return 'N/A'
return f'{dt:%Y-%m-%d %H:%M} ({time.human_timedelta(dt, accuracy=3)})'
e.add_field(name='ID', value=user.id, inline=False)
e.add_field(name='Servers', value=f'{shared} shared', inline=False)
e.add_field(name='Joined', value=format_date(getattr(user, 'joined_at', None)), inline=False)
e.add_field(name='Created', value=format_date(user.created_at), inline=False)
voice = getattr(user, 'voice', None)
if voice is not None:
vc = voice.channel
other_people = len(vc.members) - 1
voice = f'{vc.name} with {other_people} others' if other_people else f'{vc.name} by themselves'
e.add_field(name='Voice', value=voice, inline=False)
if roles:
e.add_field(name='Roles', value=', '.join(roles) if len(roles) < 10 else f'{len(roles)} roles', inline=False)
colour = user.colour
if colour.value:
e.colour = colour
if user.avatar:
e.set_thumbnail(url=user.avatar_url)
if isinstance(user, discord.User):
e.set_footer(text='This member is not in this server.')
await ctx.send(embed=e)
@commands.command(aliases=['guildinfo'], usage='')
@commands.guild_only()
async def serverinfo(self, ctx, *, guild_id: int = None):
"""Shows info about the current server."""
if guild_id is not None and await self.bot.is_owner(ctx.author):
guild = self.bot.get_guild(guild_id)
if guild is None:
return await ctx.send(f'Invalid Guild ID given.')
else:
guild = ctx.guild
roles = [role.name.replace('@', '@\u200b') for role in guild.roles]
# we're going to duck type our way here
class Secret:
pass
secret_member = Secret()
secret_member.id = 0
secret_member.roles = [guild.default_role]
# figure out what channels are 'secret'
secret = Counter()
totals = Counter()
for channel in guild.channels:
perms = channel.permissions_for(secret_member)
channel_type = type(channel)
totals[channel_type] += 1
if not perms.read_messages:
secret[channel_type] += 1
elif isinstance(channel, discord.VoiceChannel) and (not perms.connect or not perms.speak):
secret[channel_type] += 1
member_by_status = Counter(str(m.status) for m in guild.members)
e = discord.Embed()
e.title = guild.name
e.add_field(name='ID', value=guild.id)
e.add_field(name='Owner', value=guild.owner)
if guild.icon:
e.set_thumbnail(url=guild.icon_url)
channel_info = []
key_to_emoji = {
discord.TextChannel: '<:text_channel:586339098172850187>',
discord.VoiceChannel: '<:voice_channel:586339098524909604>',
}
for key, total in totals.items():
secrets = secret[key]
try:
emoji = key_to_emoji[key]
except KeyError:
continue
if secrets:
channel_info.append(f'{emoji} {total} ({secrets} locked)')
else:
channel_info.append(f'{emoji} {total}')
info = []
features = set(guild.features)
all_features = {
'PARTNERED': 'Partnered',
'VERIFIED': 'Verified',
'DISCOVERABLE': 'Server Discovery',
'PUBLIC': 'Server Discovery/Public',
'INVITE_SPLASH': 'Invite Splash',
'VIP_REGIONS': 'VIP Voice Servers',
'VANITY_URL': 'Vanity Invite',
'MORE_EMOJI': 'More Emoji',
'COMMERCE': 'Commerce',
'LURKABLE': 'Lurkable',
'NEWS': 'News Channels',
'ANIMATED_ICON': 'Animated Icon',
'BANNER': 'Banner'
}
for feature, label in all_features.items():
if feature in features:
info.append(f'{ctx.tick(True)}: {label}')
if info:
e.add_field(name='Features', value='\n'.join(info))
e.add_field(name='Channels', value='\n'.join(channel_info))
if guild.premium_tier != 0:
boosts = f'Level {guild.premium_tier}\n{guild.premium_subscription_count} boosts'
last_boost = max(guild.members, key=lambda m: m.premium_since or guild.created_at)
if last_boost.premium_since is not None:
boosts = f'{boosts}\nLast Boost: {last_boost} ({time.human_timedelta(last_boost.premium_since, accuracy=2)})'
e.add_field(name='Boosts', value=boosts, inline=False)
fmt = f'<:online:316856575413321728> {member_by_status["online"]} ' \
f'<:idle:316856575098880002> {member_by_status["idle"]} ' \
f'<:dnd:316856574868193281> {member_by_status["dnd"]} ' \
f'<:offline:316856575501402112> {member_by_status["offline"]}\n' \
f'Total: {guild.member_count}'
e.add_field(name='Members', value=fmt, inline=False)
# TODO: maybe chunk and stuff for top role members
# requires max-concurrency d.py check to work though.
e.add_field(name='Roles', value=', '.join(roles) if len(roles) < 10 else f'{len(roles)} roles')
e.set_footer(text='Created').timestamp = guild.created_at
await ctx.send(embed=e)
async def say_permissions(self, ctx, member, channel):
permissions = channel.permissions_for(member)
e = discord.Embed(colour=member.colour)
allowed, denied = [], []
for name, value in permissions:
name = name.replace('_', ' ').replace('guild', 'server').title()
if value:
allowed.append(name)
else:
denied.append(name)
e.add_field(name='Allowed', value='\n'.join(allowed))
e.add_field(name='Denied', value='\n'.join(denied))
await ctx.send(embed=e)
@commands.command()
@commands.guild_only()
async def permissions(self, ctx, member: discord.Member = None, channel: discord.TextChannel = None):
"""Shows a member's permissions in a specific channel.
If no channel is given then it uses the current one.
You cannot use this in private messages. If no member is given then
the info returned will be yours.
"""
channel = channel or ctx.channel
if member is None:
member = ctx.author
await self.say_permissions(ctx, member, channel)
@commands.command()
@commands.guild_only()
@checks.admin_or_permissions(manage_roles=True)
async def botpermissions(self, ctx, *, channel: discord.TextChannel = None):
"""Shows the bot's permissions in a specific channel.
If no channel is given then it uses the current one.
This is a good way of checking if the bot has the permissions needed
to execute the commands it wants to execute.
To execute this command you must have Manage Roles permission.
You cannot use this in private messages.
"""
channel = channel or ctx.channel
member = ctx.guild.me
await self.say_permissions(ctx, member, channel)
@commands.command(aliases=['invite'])
async def join(self, ctx):
"""Joins a server."""
perms = discord.Permissions.none()
perms.read_messages = True
perms.external_emojis = True
perms.send_messages = True
perms.manage_roles = True
perms.manage_channels = True
perms.ban_members = True
perms.kick_members = True
perms.manage_messages = True
perms.embed_links = True
perms.read_message_history = True
perms.attach_files = True
perms.add_reactions = True
await ctx.send(f'<{discord.utils.oauth_url(self.bot.client_id, perms)}>')
@commands.command(rest_is_raw=True, hidden=True)
@commands.is_owner()
async def echo(self, ctx, *, content):
await ctx.send(content)
@commands.command(hidden=True)
async def cud(self, ctx):
"""pls no spam"""
for i in range(3):
await ctx.send(3 - i)
await asyncio.sleep(1)
await ctx.send('go')
def setup(bot):
bot.add_cog(Meta(bot))

View file

@ -19,6 +19,8 @@ import psutil
from discord.ext import commands, tasks
from bot import TuxBot
from .utils import Texts
from .utils.extra import commandExtra
log = logging.getLogger(__name__)
@ -238,7 +240,8 @@ class Logs(commands.Cog):
msg = f'{emoji} `[{dt:%Y-%m-%d %H:%M:%S}] {record.message}`'
await self.webhook.send(msg)
@commands.command(name='commandstats', hidden=True)
@commandExtra(name='commandstats', hidden=True, category='logs',
description=Texts('commands').get('logs._commandstats'))
@commands.is_owner()
async def _commandstats(self, ctx, limit=20):
counter = self.bot.command_stats
@ -253,7 +256,8 @@ class Logs(commands.Cog):
await ctx.send(f'```\n{output}\n```')
@commands.command(name='socketstats', hidden=True)
@commandExtra(name='socketstats', hidden=True, category='logs',
description=Texts('commands').get('logs._socketstats'))
@commands.is_owner()
async def _socketstats(self, ctx):
delta = datetime.datetime.utcnow() - self.bot.uptime
@ -263,7 +267,8 @@ class Logs(commands.Cog):
await ctx.send(
f'{total} socket events observed ({cpm:.2f}/minute):\n{self.bot.socket_stats}')
@commands.command(name='uptime')
@commandExtra(name='uptime', category='logs',
description=Texts('commands').get('logs._uptime'))
async def _uptime(self, ctx):
"""Tells you how long the bot has been up for."""
uptime = humanize.naturaltime(

View file

@ -72,7 +72,7 @@ class Polls(commands.Cog):
await self.update_poll(poll.id)
"""---------------------------------------------------------------------"""
###########################################################################
async def create_poll(self, ctx: commands.Context, poll: str, anonymous):
question = (poll.split('|')[0]).strip()
@ -92,7 +92,7 @@ class Polls(commands.Cog):
)
for i, response in enumerate(responses):
e.add_field(
name=f"{emotes[i]} __{response.capitalize()}__",
name=f"__```{emotes[i]} - {response.capitalize()}```__",
value="**0** vote"
)
e.set_footer(text=f"ID: #{poll_row.id}")
@ -184,10 +184,9 @@ class Polls(commands.Cog):
description=Texts('commands').get('poll._poll'))
async def _poll(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
pass
await ctx.send_help('sondage')
@_poll.group(name='create', aliases=['new', 'nouveau'],
category='poll',
description=Texts('commands').get('poll._poll_create'))
async def _poll_create(self, ctx: commands.Context, *, poll: str):
is_anonymous = '--anonyme' in poll

View file

@ -7,7 +7,10 @@ from bot import TuxBot
import socket
from socket import AF_INET6
from .admin import Admin
from .utils.lang import Texts
from .utils.extra import commandExtra
class Utility(commands.Cog):
@ -15,9 +18,10 @@ class Utility(commands.Cog):
def __init__(self, bot: TuxBot):
self.bot = bot
"""---------------------------------------------------------------------"""
###########################################################################
@commands.command(name='iplocalise')
@commandExtra(name='iplocalise', category='utility',
description=Texts('commands').get('utility._iplocalise'))
async def _iplocalise(self, ctx: commands.Context, addr, ip_type=''):
addr = re.sub(r'http(s?)://', '', addr)
addr = addr[:-1] if addr.endswith('/') else addr
@ -79,9 +83,10 @@ class Utility(commands.Cog):
f"{Texts('utility', ctx).get('Cannot connect to host')} {addr}"
)
"""---------------------------------------------------------------------"""
###########################################################################
@commands.command(name='getheaders')
@commandExtra(name='getheaders', category='utility',
description=Texts('commands').get('utility._getheaders'))
async def _getheaders(self, ctx: commands.Context, addr: str):
if (addr.startswith('http') or addr.startswith('ftp')) is not True:
addr = f"http://{addr}"
@ -109,9 +114,11 @@ class Utility(commands.Cog):
f"{Texts('utility', ctx).get('Cannot connect to host')} {addr}"
)
"""---------------------------------------------------------------------"""
###########################################################################
@commands.command(name='git', aliases=['sources', 'source', 'github'])
@commandExtra(name='git', aliases=['sources', 'source', 'github'],
category='utility',
description=Texts('commands').get('utility._git'))
async def _git(self, ctx):
e = discord.Embed(
title=Texts('utility', ctx).get('git repo'),
@ -124,9 +131,10 @@ class Utility(commands.Cog):
)
await ctx.send(embed=e)
"""---------------------------------------------------------------------"""
###########################################################################
@commands.command(name='quote')
@commandExtra(name='quote', category='utility',
description=Texts('commands').get('utility._quote'))
async def _quote(self, ctx, message_id: discord.Message):
e = discord.Embed(
colour=message_id.author.colour,

View file

@ -1,23 +1,15 @@
import configparser
class Config:
class Config(configparser.RawConfigParser):
__slots__ = ('name', '_db')
def __init__(self, name):
super().__init__()
self.name = name
self._db: configparser.ConfigParser = configparser.ConfigParser()
self._db = super()
self._db.read(self.name)
def __contains__(self, item):
return item in self._db
def __getitem__(self, item):
return self._db[item]
def all(self) -> list:
return self._db.sections()
def get(self, *args, **kwargs) -> str:
return self._db.get(*args, **kwargs)

View file

@ -4,7 +4,7 @@ from discord.ext import commands
class commandsPlus(commands.Command):
def __init__(self, func, **kwargs):
super().__init__(func, **kwargs)
self.category = kwargs.pop("category")
self.category = kwargs.get("category", 'other')
def commandExtra(*args, **kwargs):
@ -14,7 +14,7 @@ def commandExtra(*args, **kwargs):
class GroupPlus(commands.Group):
def __init__(self, func, **kwargs):
super().__init__(func, **kwargs)
self.category = kwargs.pop("category")
self.category = kwargs.get("category", 'other')
def groupExtra(*args, **kwargs):

316
cogs/utils/paginator.py Normal file
View file

@ -0,0 +1,316 @@
import asyncio
import discord
from discord.ext.commands import Paginator as CommandPaginator
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=12, show_entry_count=True):
self.bot = ctx.bot
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=discord.Colour.blurple())
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]
def get_content(self, entries, page, *, first=False):
return None
def get_embed(self, entries, page, *, first=False):
self.prepare_embed(entries, page, first=first)
return self.embed
def prepare_embed(self, entries, page, *, first=False):
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 self.paginating and first:
p.append('')
p.append(
'Confused? React with \N{INFORMATION SOURCE} for more info.')
self.embed.description = '\n'.join(p)
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
content = self.get_content(entries, page, first=first)
embed = self.get_embed(entries, page, first=first)
if not self.paginating:
return await self.channel.send(content=content, embed=embed)
if not first:
await self.message.edit(content=content, embed=embed)
return
self.message = await self.channel.send(content=content, embed=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):
"""goes to the first page"""
await self.show_page(1)
async def last_page(self):
"""goes to the last page"""
await self.show_page(self.maximum_pages)
async def next_page(self):
"""goes to the next page"""
await self.checked_show_page(self.current_page + 1)
async def previous_page(self):
"""goes to the 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):
"""lets you type a page number to go to"""
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__}')
embed = self.embed.copy()
embed.clear_fields()
embed.description = '\n'.join(messages)
embed.set_footer(
text=f'We were on page {self.current_page} before this message.')
await self.message.edit(content=None, embed=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):
"""stops the interactive pagination session"""
await self.message.delete()
self.paginating = False
def react_check(self, payload):
if payload.user_id != self.author.id:
return False
if payload.message_id != self.message.id:
return False
to_check = str(payload.emoji)
for (emoji, func) in self.reaction_emojis:
if to_check == 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:
payload = await self.bot.wait_for('raw_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(payload.emoji,
discord.Object(
id=payload.user_id))
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.
"""
def prepare_embed(self, entries, page, *, first=False):
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)
class TextPages(Pages):
"""Uses a commands.Paginator internally to paginate some text."""
def __init__(self, ctx, text, *, prefix='```', suffix='```',
max_size=2000):
paginator = CommandPaginator(prefix=prefix, suffix=suffix,
max_size=max_size - 200)
for line in text.split('\n'):
paginator.add_line(line)
super().__init__(ctx, entries=paginator.pages, per_page=1,
show_entry_count=False)
def get_page(self, page):
return self.entries[page - 1]
def get_embed(self, entries, page, *, first=False):
return None
def get_content(self, entry, page, *, first=False):
if self.maximum_pages > 1:
return f'{entry}\nPage {page}/{self.maximum_pages}'
return entry

View file

@ -1,2 +1,12 @@
[280805240977227776]
prefixes = b.
prefixes = b1.|Imo07fZY9ogan7ank1n3UERg|b2.
[303633056944881686]
prefixes = b1.
[373881878471770112]
prefixes = b1.
[336642139381301249]
prefixes =

View file

@ -1,3 +1,4 @@
humanize
discord.py[voice]
jishaku
gitpython
@ -5,4 +6,5 @@ sqlalchemy
psycopg2
configparser
psutil
tcp_latency
tcp_latency
yarl