|
| 1 | +import discord |
| 2 | +from discord.ext import commands |
| 3 | +import traceback |
| 4 | +import inspect |
| 5 | +import io |
| 6 | +import textwrap |
| 7 | +from contextlib import redirect_stdout |
| 8 | +from difflib import get_close_matches |
| 9 | + |
| 10 | +from core.paginator import PaginatorSession |
| 11 | +from core.decorators import auth_required, owner_only, trigger_typing |
| 12 | + |
| 13 | +class Utility: |
| 14 | + '''General commands that provide utility''' |
| 15 | + |
| 16 | + def __init__(self, bot): |
| 17 | + self.bot = bot |
| 18 | + |
| 19 | + def format_cog_help(self, ctx, cog): |
| 20 | + """Formats the text for a cog help""" |
| 21 | + sigs = [] |
| 22 | + prefix = self.bot.config.get('PREFIX', 'm.') |
| 23 | + |
| 24 | + for cmd in self.bot.commands: |
| 25 | + if cmd.hidden: |
| 26 | + continue |
| 27 | + if cmd.instance is cog: |
| 28 | + sigs.append(len(cmd.qualified_name) + len(prefix)) |
| 29 | + if hasattr(cmd, 'all_commands'): |
| 30 | + for c in cmd.all_commands.values(): |
| 31 | + sigs.append(len('\u200b └─ ' + c.name) + 1) |
| 32 | + |
| 33 | + if not sigs: |
| 34 | + return |
| 35 | + |
| 36 | + maxlen = max(sigs) |
| 37 | + |
| 38 | + fmt = [''] |
| 39 | + index = 0 |
| 40 | + for cmd in self.bot.commands: |
| 41 | + if cmd.instance is cog: |
| 42 | + if cmd.hidden: |
| 43 | + continue |
| 44 | + if len(fmt[index] + f'`{prefix+cmd.qualified_name:<{maxlen}}` - ' + f'{cmd.short_doc:<{maxlen}}\n') > 1024: |
| 45 | + index += 1 |
| 46 | + fmt.append('') |
| 47 | + fmt[index] += f'`{prefix+cmd.qualified_name:<{maxlen}}` - ' |
| 48 | + fmt[index] += f'{cmd.short_doc:<{maxlen}}\n' |
| 49 | + if hasattr(cmd, 'commands'): |
| 50 | + for c in cmd.commands: |
| 51 | + branch = '\u200b └─ ' + c.name |
| 52 | + if len(fmt[index] + f"`{branch:<{maxlen+1}}` - " + f"{c.short_doc:<{maxlen}}\n") > 1024: |
| 53 | + index += 1 |
| 54 | + fmt.append('') |
| 55 | + fmt[index] += f"`{branch:<{maxlen+1}}` - " |
| 56 | + fmt[index] += f"{c.short_doc:<{maxlen}}\n" |
| 57 | + |
| 58 | + em = discord.Embed( |
| 59 | + description='*' + inspect.getdoc(cog) + '*', |
| 60 | + color=discord.Colour.green() |
| 61 | + ) |
| 62 | + em.set_author(name=cog.__class__.__name__ + ' - Help', icon_url=ctx.bot.user.avatar_url) |
| 63 | + |
| 64 | + for n, i in enumerate(fmt): |
| 65 | + if n == 0: |
| 66 | + em.add_field(name='Commands', value=i) |
| 67 | + else: |
| 68 | + em.add_field(name=u'\u200b', value=i) |
| 69 | + |
| 70 | + em.set_footer(text=f'Type {prefix}command for more info on a command.') |
| 71 | + return em |
| 72 | + |
| 73 | + def format_command_help(self, ctx, cmd): |
| 74 | + '''Formats command help.''' |
| 75 | + prefix = self.bot.config.get('PREFIX', 'm.') |
| 76 | + em = discord.Embed( |
| 77 | + color=discord.Color.green(), |
| 78 | + description=cmd.help |
| 79 | + ) |
| 80 | + |
| 81 | + if hasattr(cmd, 'invoke_without_command') and cmd.invoke_without_command: |
| 82 | + em.title = f'`Usage: {prefix}{cmd.signature}`' |
| 83 | + else: |
| 84 | + em.title = f'`{prefix}{cmd.signature}`' |
| 85 | + |
| 86 | + if not hasattr(cmd, 'commands'): |
| 87 | + return em |
| 88 | + |
| 89 | + maxlen = max(len(prefix + str(c)) for c in cmd.commands) |
| 90 | + fmt = '' |
| 91 | + |
| 92 | + for i, c in enumerate(cmd.commands): |
| 93 | + if len(cmd.commands) == i + 1: # last |
| 94 | + branch = '└─ ' + c.name |
| 95 | + else: |
| 96 | + branch = '├─ ' + c.name |
| 97 | + fmt += f"`{branch:<{maxlen+1}}` - " |
| 98 | + fmt += f"{c.short_doc:<{maxlen}}\n" |
| 99 | + |
| 100 | + em.add_field(name='Subcommands', value=fmt) |
| 101 | + em.set_footer(text=f'Type {prefix}help {cmd} command for more info on a command.') |
| 102 | + |
| 103 | + return em |
| 104 | + |
| 105 | + def format_not_found(self, ctx, command): |
| 106 | + prefix = ctx.prefix |
| 107 | + em = discord.Embed() |
| 108 | + em.title = 'Could not find a cog or command by that name.' |
| 109 | + em.color = discord.Color.green() |
| 110 | + em.set_footer(text=f'Type {prefix}help to get a full list of commands.') |
| 111 | + cogs = get_close_matches(command, self.bot.cogs.keys()) |
| 112 | + cmds = get_close_matches(command, self.bot.all_commands.keys()) |
| 113 | + if cogs or cmds: |
| 114 | + em.description = 'Did you mean...' |
| 115 | + if cogs: |
| 116 | + em.add_field(name='Cogs', value='\n'.join(f'`{x}`' for x in cogs)) |
| 117 | + if cmds: |
| 118 | + em.add_field(name='Commands', value='\n'.join(f'`{x}`' for x in cmds)) |
| 119 | + return em |
| 120 | + |
| 121 | + @commands.command() |
| 122 | + async def help(self, ctx, *, command: str=None): |
| 123 | + """Shows the help message.""" |
| 124 | + |
| 125 | + await ctx.trigger_typing() |
| 126 | + |
| 127 | + if command is not None: |
| 128 | + cog = self.bot.cogs.get(command) |
| 129 | + cmd = self.bot.get_command(command) |
| 130 | + if cog is not None: |
| 131 | + em = self.format_cog_help(ctx, cog) |
| 132 | + elif cmd is not None: |
| 133 | + em = self.format_command_help(ctx, cmd) |
| 134 | + else: |
| 135 | + em = self.format_not_found(ctx, command) |
| 136 | + if em: |
| 137 | + return await ctx.send(embed=em) |
| 138 | + |
| 139 | + pages = [] |
| 140 | + |
| 141 | + prefix = self.bot.config.get('PREFIX', 'm.') |
| 142 | + |
| 143 | + for _, cog in sorted(self.bot.cogs.items()): |
| 144 | + em = self.format_cog_help(ctx, cog) |
| 145 | + if em: |
| 146 | + pages.append(em) |
| 147 | + |
| 148 | + p_session = PaginatorSession(ctx, *pages) |
| 149 | + |
| 150 | + await p_session.run() |
| 151 | + |
| 152 | + @commands.command() |
| 153 | + @trigger_typing |
| 154 | + async def about(self, ctx): |
| 155 | + '''Shows information about the bot.''' |
| 156 | + em = discord.Embed(color=discord.Color.green(), timestamp=datetime.datetime.utcnow()) |
| 157 | + em.set_author(name='Mod Mail - Information', icon_url=self.bot.user.avatar_url) |
| 158 | + em.set_thumbnail(url=self.bot.user.avatar_url) |
| 159 | + |
| 160 | + em.description = 'This is an open source discord bot made by kyb3r and '\ |
| 161 | + 'improved upon suggestions by the users! This bot serves as a means for members to '\ |
| 162 | + 'easily communicate with server leadership in an organised manner.' |
| 163 | + |
| 164 | + try: |
| 165 | + async with self.bot.session.get('https://api.modmail.tk/metadata') as resp: |
| 166 | + meta = await resp.json() |
| 167 | + except: |
| 168 | + meta = None |
| 169 | + |
| 170 | + em.add_field(name='Uptime', value=self.bot.uptime) |
| 171 | + if meta: |
| 172 | + em.add_field(name='Instances', value=meta['instances']) |
| 173 | + else: |
| 174 | + em.add_field(name='Latency', value=f'{self.bot.latency*1000:.2f} ms') |
| 175 | + |
| 176 | + |
| 177 | + em.add_field(name='Version', value=f'[`{__version__}`](https://github.com/kyb3r/modmail/blob/master/bot.py#L25)') |
| 178 | + em.add_field(name='Author', value='[`kyb3r`](https://github.com/kyb3r)') |
| 179 | + |
| 180 | + em.add_field(name='Latest Updates', value=await self.bot.get_latest_updates()) |
| 181 | + |
| 182 | + footer = f'Bot ID: {self.bot.user.id}' |
| 183 | + |
| 184 | + if meta: |
| 185 | + if __version__ != meta['latest_version']: |
| 186 | + footer = f"A newer version is available v{meta['latest_version']}" |
| 187 | + else: |
| 188 | + footer = 'You are up to date with the latest version.' |
| 189 | + |
| 190 | + em.add_field(name='Github', value='https://github.com/kyb3r/modmail', inline=False) |
| 191 | + |
| 192 | + em.set_footer(text=footer) |
| 193 | + |
| 194 | + await ctx.send(embed=em) |
| 195 | + |
| 196 | + @commands.command() |
| 197 | + @owner_only() |
| 198 | + @auth_required |
| 199 | + @trigger_typing |
| 200 | + async def github(self, ctx): |
| 201 | + '''Shows the github user your access token is linked to.''' |
| 202 | + if ctx.invoked_subcommand: |
| 203 | + return |
| 204 | + |
| 205 | + data = await self.bot.modmail_api.get_user_info() |
| 206 | + print(data) |
| 207 | + |
| 208 | + prefix = self.bot.config.get('PREFIX', 'm.') |
| 209 | + |
| 210 | + em = discord.Embed(title='Github') |
| 211 | + |
| 212 | + user = data['user'] |
| 213 | + em.color = discord.Color.green() |
| 214 | + em.description = f"Current user." |
| 215 | + em.set_author(name=user['username'], icon_url=user['avatar_url'], url=user['url']) |
| 216 | + em.set_thumbnail(url=user['avatar_url']) |
| 217 | + await ctx.send(embed=em) |
| 218 | + |
| 219 | + |
| 220 | + @commands.command() |
| 221 | + @owner_only() |
| 222 | + @auth_required |
| 223 | + @trigger_typing |
| 224 | + async def update(self, ctx): |
| 225 | + '''Updates the bot, this only works with heroku users.''' |
| 226 | + metadata = await self.bot.modmail_api.get_metadata() |
| 227 | + |
| 228 | + em = discord.Embed( |
| 229 | + title='Already up to date', |
| 230 | + description=f'The latest version is [`{__version__}`](https://github.com/kyb3r/modmail/blob/master/bot.py#L25)', |
| 231 | + color=discord.Color.green() |
| 232 | + ) |
| 233 | + |
| 234 | + if metadata['latest_version'] == __version__: |
| 235 | + data = await self.bot.modmail_api.get_user_info() |
| 236 | + if not data['error']: |
| 237 | + user = data['user'] |
| 238 | + em.set_author(name=user['username'], icon_url=user['avatar_url'], url=user['url']) |
| 239 | + else: |
| 240 | + data = await self.bot.modmail_api.update_repository() |
| 241 | + |
| 242 | + commit_data = data['data'] |
| 243 | + user = data['user'] |
| 244 | + em.title = 'Success' |
| 245 | + em.set_author(name=user['username'], icon_url=user['avatar_url'], url=user['url']) |
| 246 | + em.set_footer(text=f"Updating modmail v{__version__} -> v{metadata['latest_version']}") |
| 247 | + |
| 248 | + if commit_data: |
| 249 | + em.description = 'Bot successfully updated, the bot will restart momentarily' |
| 250 | + message = commit_data['commit']['message'] |
| 251 | + html_url = commit_data["html_url"] |
| 252 | + short_sha = commit_data['sha'][:6] |
| 253 | + em.add_field(name='Merge Commit', value=f"[`{short_sha}`]({html_url}) {message} - {user['username']}") |
| 254 | + else: |
| 255 | + em.description = 'Already up to date with master repository.' |
| 256 | + |
| 257 | + em.add_field(name='Latest Commit', value=await self.bot.get_latest_updates(limit=1), inline=False) |
| 258 | + |
| 259 | + await ctx.send(embed=em) |
| 260 | + |
| 261 | + @commands.command(name="status", aliases=['customstatus', 'presence']) |
| 262 | + @commands.has_permissions(administrator=True) |
| 263 | + async def _status(self, ctx, *, message): |
| 264 | + '''Set a custom playing status for the bot.''' |
| 265 | + if message == 'clear': |
| 266 | + return await self.bot.change_presence(activity=None) |
| 267 | + await self.bot.change_presence(activity=discord.Game(message)) |
| 268 | + em = discord.Embed(title='Status Changed') |
| 269 | + em.description = message |
| 270 | + em.color = discord.Color.green() |
| 271 | + em.set_footer(text='Note: this change is temporary.') |
| 272 | + await ctx.send(embed=em) |
| 273 | + |
| 274 | + @commands.command() |
| 275 | + @trigger_typing |
| 276 | + @commands.has_permissions(administrator=True) |
| 277 | + async def ping(self, ctx): |
| 278 | + """Pong! Returns your websocket latency.""" |
| 279 | + em = discord.Embed() |
| 280 | + em.title ='Pong! Websocket Latency:' |
| 281 | + em.description = f'{self.bot.ws.latency * 1000:.4f} ms' |
| 282 | + em.color = 0x00FF00 |
| 283 | + await ctx.send(embed=em) |
| 284 | + |
| 285 | + @commands.command(hidden=True, name='eval') |
| 286 | + @owner_only() |
| 287 | + async def _eval(self, ctx, *, body): |
| 288 | + """Evaluates python code""" |
| 289 | + env = { |
| 290 | + 'ctx': ctx, |
| 291 | + 'bot': self.bot, |
| 292 | + 'channel': ctx.channel, |
| 293 | + 'author': ctx.author, |
| 294 | + 'guild': ctx.guild, |
| 295 | + 'message': ctx.message, |
| 296 | + 'source': inspect.getsource, |
| 297 | + } |
| 298 | + |
| 299 | + env.update(globals()) |
| 300 | + |
| 301 | + def cleanup_code(content): |
| 302 | + """Automatically removes code blocks from the code.""" |
| 303 | + # remove ```py\n``` |
| 304 | + if content.startswith('```') and content.endswith('```'): |
| 305 | + return '\n'.join(content.split('\n')[1:-1]) |
| 306 | + |
| 307 | + # remove `foo` |
| 308 | + return content.strip('` \n') |
| 309 | + |
| 310 | + body = cleanup_code(body) |
| 311 | + stdout = io.StringIO() |
| 312 | + err = out = None |
| 313 | + |
| 314 | + to_compile = f'async def func():\n{textwrap.indent(body, " ")}' |
| 315 | + |
| 316 | + def paginate(text: str): |
| 317 | + '''Simple generator that paginates text.''' |
| 318 | + last = 0 |
| 319 | + pages = [] |
| 320 | + for curr in range(0, len(text)): |
| 321 | + if curr % 1980 == 0: |
| 322 | + pages.append(text[last:curr]) |
| 323 | + last = curr |
| 324 | + appd_index = curr |
| 325 | + if appd_index != len(text) - 1: |
| 326 | + pages.append(text[last:curr]) |
| 327 | + return list(filter(lambda a: a != '', pages)) |
| 328 | + |
| 329 | + try: |
| 330 | + exec(to_compile, env) |
| 331 | + except Exception as e: |
| 332 | + err = await ctx.send(f'```py\n{e.__class__.__name__}: {e}\n```') |
| 333 | + return await ctx.message.add_reaction('\u2049') |
| 334 | + |
| 335 | + func = env['func'] |
| 336 | + try: |
| 337 | + with redirect_stdout(stdout): |
| 338 | + ret = await func() |
| 339 | + except Exception as e: |
| 340 | + value = stdout.getvalue() |
| 341 | + err = await ctx.send(f'```py\n{value}{traceback.format_exc()}\n```') |
| 342 | + else: |
| 343 | + value = stdout.getvalue() |
| 344 | + if ret is None: |
| 345 | + if value: |
| 346 | + try: |
| 347 | + out = await ctx.send(f'```py\n{value}\n```') |
| 348 | + except: |
| 349 | + paginated_text = paginate(value) |
| 350 | + for page in paginated_text: |
| 351 | + if page == paginated_text[-1]: |
| 352 | + out = await ctx.send(f'```py\n{page}\n```') |
| 353 | + break |
| 354 | + await ctx.send(f'```py\n{page}\n```') |
| 355 | + else: |
| 356 | + try: |
| 357 | + out = await ctx.send(f'```py\n{value}{ret}\n```') |
| 358 | + except: |
| 359 | + paginated_text = paginate(f"{value}{ret}") |
| 360 | + for page in paginated_text: |
| 361 | + if page == paginated_text[-1]: |
| 362 | + out = await ctx.send(f'```py\n{page}\n```') |
| 363 | + break |
| 364 | + await ctx.send(f'```py\n{page}\n```') |
| 365 | + |
| 366 | + if out: |
| 367 | + await ctx.message.add_reaction('\u2705') # tick |
| 368 | + elif err: |
| 369 | + await ctx.message.add_reaction('\u2049') # x |
| 370 | + else: |
| 371 | + await ctx.message.add_reaction('\u2705') |
| 372 | + |
| 373 | + |
| 374 | +def setup(bot): |
| 375 | + bot.add_cog(Utility(bot)) |
0 commit comments