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 = ( ('', '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} ' 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))