From 9da190a078d118ae6cf6c85e2392404aa990129a Mon Sep 17 00:00:00 2001 From: Adriene Hutchins Date: Fri, 28 Feb 2020 23:14:34 -0500 Subject: [PATCH] Complete rearranging for modularity and dev cmds --- extensions/__pycache__/core.cpython-38.pyc | Bin 0 -> 5809 bytes extensions/core.py | 170 ++++++ extensions/developer.py | 576 ++++++++++++++++++--- extensions/search.py | 85 ++- main.py | 35 +- 5 files changed, 745 insertions(+), 121 deletions(-) create mode 100644 extensions/__pycache__/core.cpython-38.pyc create mode 100644 extensions/core.py diff --git a/extensions/__pycache__/core.cpython-38.pyc b/extensions/__pycache__/core.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c3ed9eeb047d145c75ef5ef18fe732df6971050 GIT binary patch literal 5809 zcmb7I%X8bt8OP#95R_zDem`O(PU=(~mvUZBN8_Z4WXEw6tFGmw38oE2ahDP#5?~gf zZNXG~uzSf&{({sU>F7(R)9Io2=FrKZ#~#pQFZE0hz1D52{=Nkyl8PrYp|l@%vERPF z@Ao|x?~jkW8h-!&+eaTy9@DhHQ)BpNqVXn5yavKG&f1!vKbg zSiYs6wr{Iv$uFs=<2&kE_RCi_ZgBH~#!X?Yx%Umf!Yyt;(EKsZ7PaZpW2~nwOtZ{s zMx9P0{rEa(T*oX%KD%!Ui(9{D+~%bRrf-YVn!_Dl=I)BY zE1xkw#>XFM_nBW-Jrn!@pDcO~@+sBhiqe*|t?@(ruxeL|_7Q$mwa4-{==ipV)sA7c zy2=o=Fb#d2Nc&Ce)cI`9)Gsrnp9lp_;bTt{CWNz+6UG; z$j?J^FJ$I?Bt=sn5*5{{%#*ZEgY;Qnt3#VBT0k|FEqz-fEn)_JA#=suM1*k=h4HkJ z8Ou?UIk8BRAY6&F$$Ah5NqvY#&&*h~Taq;LNRwqep1e7;8g;}>(r5-rPd4Jl%;l)r z>xeLkXKu)7O*B!7MME}MG5yS7wwWfjU+ZpUPCZ|!K0(`7IAyl3Yx<6DrsH(T+Ac>E z2-y44NlPf|v=Ws>+hUw;v&-7gmp56TZRuR!X3|9sIN4^gn_lW`Yvz{4%|0U>{z^Zg zeXczO7t_#Vb9O@8Dm|_Dr`jhjgXEa@5o#ZzaEtJp9=ir11&wx)ir5>Rt|rN(QiBd? z(n-_GQ0KzquXc8JrkzZW;;hsT;)Dk>a|GajCrDN^H%=NdQFSHkY@!`C_&^oX#^jmZ z=yow=T9*zr4J@E^k`of7B>5agiKjp`V}c!KIy=c+Ho4>IHcOA>;@z)kmf2E3o#6<) zXi+Lk;QvA;0l@CFO_l&+cvsiL zMR6Gg7u2p+WAcl>1+KpqTw{vMEV!`8bteDG$JJXaf6FJJ?gQ!Nn@TQTvMRil;I;_8 z<&6ZIZ{Xis?ZJb2Eg3*7?G5kUxeMNPl&dI_^lr_0aUx+_wLk+n9us~a8$=XTO7|{q zdiP%3l!WoeM0{(G=Jri`r0%_ov$dC6o0xV`zdlsII#jM6w>T`F z%<(iBo|!?|iZVNnda^0-6!Nx^@b@K{Od}TY2*OAjB#b|y#2kdfWvyvz>~n;mhsLJf z*SVqI=xYYvnGcQEH1*aho_6sp6;G$n)=Y4g3r@CW^szm%smNwR#~R6NE70 zuqkq`)T&{qn_j6_7nLZy`G||zYSpV>T1}E}JUcV90@Lp;*ANJY-RYk1{2mWtcoP0b z9b3ROy>2&>iMOaEIyzIqSzB3|d1bD9Q!IQygMLPe!$LCoaUcu<8HxOmxjJigoUBw|6aw-{ui;QjtD@aq z^1$d(?Si)ygtvo4EYWHW&eckg>IE#qy(LVY#5p@v;SYmmT&phtvJ`8w3I|YR&?UV9 zIo+-Tl@;9yh0N@(6s_Pc#io!yG74wdOzvhzCtlf)ZA+>3H3AsL8_k?#M}celo6=KP z;a9QvHugyou>|Ra2xFPK76qdok?=|zRCs*_m0i59>4aE?RD>Q1GQg?@q_Qouz@Me? zM?u6+Yga`=4x}A~>m;FEShG~F zpCzEZ4+WDpTT#0m-J#Cixk>sRG3!?Ma95xOVpZ6S8ogxor5U1;L|FS_vkixq)C2x{ zI)1(@WG9GK%J8~x-MDh|`eHhE9>DC1*Rj<%steOrt~9fU?PhwwGaC|Y1uODABqU!T zLaD>4 zAZw?fj+R-p5c1KoRLj`1x3#qr+BW{-)Z)w>|$=eoY0Z$veEI!8@uPFW@h!` z#tH%j2JHunrM$%r^u!dLG}A#0qQ6O|>-zspL;uP&_5Yfd`ybPGpE%{hs@1R&7!lO= z%=bMsc44G)M`V&vtp1Kh$&7-R*@y8S$BpK&1^^jrxY=x(iPg96gO^*M75oS*NL*mn zr{>8rW2JbwtQB&q1qfB*%V*A}jNqju=s+w8`Mhm_j4QytgSmj$7Pa!wY zz#-I074FlrGC~@GsBMOgj%Z@fdq0xlo+ep29hbXVTLfoHJzI$ zwDpqwefknwFxb9XSmO%?lOD{qbxcAYD+<}oo z8*63M%gU79ho#Z+uR=SU)-S9NDU<)=LoJ+A<152lN`GVN+4qojBSH=GdU$5|;yoQY z#6XH&crr0CvKlhEJpDg-W%qvNQ35Y`P5uBPN)KO=5~1KjdwGb+C6KIqHl;ciN`4(ug<;UL+D}`o9^shAQq|d$|(k4fB+mcadpj$U*QQDRB*lMoQLj%q9 z)Es+CJvZoqc_Q-mklH}Y)S$({I9+<0^0LW%rosJ=u3w!9p`Mh9cuMh3IZ&lB`6Ft+ zOJsi>%_sULy7KJC%JoEAI;M zvasYo6qY=T5xM<4L-t=vV9Bs)m+V{Fbot>vpD<$6U+&qo`!$=cq-Sr6uWNqTFS|Lz zH|Sle8TRyk`fUViL}fUJyl3Dp+$Bjljl2Sq9vk2zh{G2Kpr||z6mo$EDzhdK$hV37 zn8;Nk?-030WIqJ*J$#0)_#}w72LUoxGF39xqhzdkdX@7dh};blAGlkNqGTjWQamS* zuP{MV=9t*^?MzG9eY#3f%rnxdTlj2{hdjziqE@Sx9+^$kbQX7mn*u>t(&r`IhHyj8 z4}KGK79TNkkncNE2{{W5+X7!q-b1q{SvscTQqi?AJ*7mFx%GM{;&46ndNx+Ce}Z%- z_j{6pl=>c$8Mub4G$v0~5MR}oM_DC~i$v&ppaNp%1aVzmGvsx8M_Gsb36Vu2gN>>h zS&k%8jiJOf5Jz_$XKdogBy-15DmHTup_FZAv#A3UM8_08NK}m-`Nl<`0qDf@G$DZj zX)EO2$0rtwm+CtnqpN-iX?-0zmzrXl str: # function lifted from StackOverflow + """Return the given bytes as a human friendly KB, MB, GB, or TB string.""" + + B = float(B) + KB = float(1024) + MB = float(KB ** 2) # 1,048,576 + GB = float(KB ** 3) # 1,073,741,824 + TB = float(KB ** 4) # 1,099,511,627,776 + + if B < KB: + return '{0} {1}'.format( + B, 'Bytes' if 0 == B > 1 else 'Byte') + elif KB <= B < MB: + return '{0:.2f} KB'.format(B/KB) + elif MB <= B < GB: + return '{0:.2f} MB'.format(B/MB) + elif GB <= B < TB: + return '{0:.2f} GB'.format(B/GB) + elif TB <= B: + return '{0:.2f} TB'.format(B/TB) + + @commands.command(aliases=['info', 'source', 'server']) + async def about(self, ctx): + """Returns information about this bot.""" + + msg = f"**{self.bot.description}**\n" + msg += f"Created by **taciturasa#4365**, this instance by **{self.bot.appinfo.owner}.**\n\n" + msg += "**Source Code:** __\n" + msg += "**Support Server:** __\n" + msg += "_Note: Please attempt to contact the hoster of any separate instances before this server._\n\n" + msg += f"_See **{ctx.prefix}** `help` for help, `invite` to add the bot, and `stats` for statistics._" + + await ctx.send(msg) + + @commands.command(aliases=['addbot', 'connect', 'join']) + async def invite(self, ctx): + """Gets a link to invite this bot to your server.""" + + msg = ( + "**Thanks for checking me out!**\n\n" + "Use the following link to add me:\n" + f"**" + ) + + await ctx.send(msg) + + @commands.command() + async def stats(self, ctx): + """Provides statistics on the bot itself.""" + + mem = psutil.virtual_memory() + currproc = psutil.Process(os.getpid()) + total_ram = self._humanbytes(mem[0]) + available_ram = self._humanbytes(mem[1]) + usage = self._humanbytes(currproc.memory_info().rss) + msg = f""" +``` +Total RAM: {total_ram} +Available RAM: {available_ram} +RAM used by bot: {usage} +Number of bot commands: {len(ctx.bot.commands)} +Number of extensions present: {len(ctx.bot.cogs)} +``` +""" + await ctx.send(msg) + + @commands.command() + async def ping(self, ctx): + """Checks the ping of the bot.""" + + before = time.monotonic() + pong = await ctx.send("...") + after = time.monotonic() + ping = (after - before) * 1000 + await pong.edit(content="`PING discordapp.com {}ms`".format(int(ping))) + + @commands.command() + @commands.is_owner() + async def load(self, ctx, name: str): + """Load an extension into the bot.""" + m = await ctx.send(f'Loading {name}') + extension_name = 'extensions.{0}'.format(name) + if extension_name not in self.settings['extensions']: + try: + self.bot.load_extension(extension_name) + self.settings['extensions'].append(extension_name) + await m.edit(content='Extension loaded.') + except Exception as e: + await m.edit( + content=f'Error while loading {name}\n`{type(e).__name__}: {e}`') + else: + await m.edit(content='Extension already loaded.') + + @commands.command(aliases=["ule", "ul"]) + @commands.is_owner() + async def unload(self, ctx, name: str): + """Unload an extension from the bot.""" + + m = await ctx.send(f'Unloading {name}') + extension_name = 'extensions.{0}'.format(name) + if extension_name in self.settings['extensions']: + self.bot.unload_extension(extension_name) + self.settings['extensions'].remove(extension_name) + await m.edit(content='Extension unloaded.') + else: + await m.edit(content='Extension not found or not loaded.') + + @commands.command(aliases=["rle", "rl"]) + @commands.is_owner() + async def reload(self, ctx, name: str): + """Reload an extension of the bot.""" + + m = await ctx.send(f'Reloading {name}') + extension_name = 'extensions.{0}'.format(name) + if extension_name in self.settings['extensions']: + self.bot.unload_extension(extension_name) + try: + self.bot.load_extension(extension_name) + await m.edit(content='Extension reloaded.') + except Exception as e: + self.settings['extensions'].remove(extension_name) + await m.edit( + content=f'Failed to reload extension\n`{type(e).__name__}: {e}`') + else: + await m.edit(content='Extension isn\'t loaded.') + + @commands.command(aliases=['exit', 'reboot']) + @commands.is_owner() + async def restart(self, ctx): + """Turns the bot off.""" + + await ctx.send(':zzz: **Restarting.**') + exit() + + +def setup(bot): + bot.add_cog(Core(bot)) diff --git a/extensions/developer.py b/extensions/developer.py index f2a653a..c395e5e 100644 --- a/extensions/developer.py +++ b/extensions/developer.py @@ -8,111 +8,519 @@ import discord from discord.ext import commands import os -# import psutil import aiohttp import random +import datetime +import collections +from contextlib import redirect_stdout +import traceback +import time +import io +import inspect +import textwrap +import subprocess + class Developer(commands.Cog): - def __init__(self, bot): + def __init__(self, bot): self.bot = bot self.request = bot.request self.instances = bot.instances + self.repl_sessions = {} + self.repl_embeds = {} + self._eval = {} - async def _instance_check(self, instance, info): - '''Checks the quality of an instance.''' + def _cleanup_code(self, content): + """Automatically removes code blocks from the code.""" + # remove ```py\n``` + if content.startswith('```') and content.endswith('```'): + return '\n'.join(content.split('\n')[1:-1]) - # Makes sure proper values exist - if 'error' in info: - return False - if not ('engines' in info and 'initial' in info['timing']): - return False - if not ('google' in info['engines'] and 'enabled' in info['engines']['google']): - return False + # remove `foo` + return content.strip('` \n') - # Makes sure google is enabled - if not info['engines']['google']['enabled']: - return False + def _get_syntax_error(self, err): + """Returns SyntaxError formatted for repl reply.""" + return '```py\n{0.text}{1:>{0.offset}}\n{2}: {0}```'.format( + err, + '^', + type(err).__name__) - # Makes sure is not Tor - if info['network_type'] != 'normal': - return False + async def _post_to_hastebin(self, string): + """Posts a string to hastebin.""" + url = "https://hastebin.com/documents" + data = string.encode('utf-8') + async with self.request.post(url=url, data=data) as haste_response: + haste_key = (await haste_response.json())['key'] + haste_url = f"http://hastebin.com/{haste_key}" + # data = {'sprunge': ''} + # data['sprunge'] = string + # haste_url = await self.aioclient.post(url='http://sprunge.us', + # data=data) + return haste_url - # Only picks instances that are fast enough - timing = int(info['timing']['initial']) - if timing > 0.20: - return False + @commands.group(name='shell', + aliases=['ipython', 'repl', 'longexec']) + async def repl(self, ctx, *, name: str = None): + """Head on impact with an interactive python shell.""" + # TODO Minimize local variables + # TODO Minimize branches + + session = ctx.message.channel.id + + embed = discord.Embed( + description="_Enter code to execute or evaluate. " + "`exit()` or `quit` to exit._", + timestamp=datetime.datetime.now()) + + embed.set_footer( + text="Interactive Python Shell", + icon_url="https://upload.wikimedia.org/wikipedia/commons/thumb" + "/c/c3/Python-logo-notext.svg/1024px-Python-logo-notext.svg.png") + + if name is not None: + embed.title = name.strip(" ") + + history = collections.OrderedDict() + + variables = { + 'ctx': ctx, + 'bot': self.bot, + 'message': ctx.message, + 'server': ctx.message.guild, + 'channel': ctx.message.channel, + 'author': ctx.message.author, + 'discord': discord, + '_': None + } + + if session in self.repl_sessions: + error_embed = discord.Embed( + color=15746887, + description="**Error**: " + "_Shell is already running in channel._") + await ctx.send(embed=error_embed) + return + + shell = await ctx.send(embed=embed) + + self.repl_sessions[session] = shell + self.repl_embeds[shell] = embed + + while True: + response = await self.bot.wait_for( + 'message', + check=lambda m: m.content.startswith( + '`') and m.author == ctx.author and m.channel == ctx.channel + ) + cleaned = self._cleanup_code(response.content) + shell = self.repl_sessions[session] + + # Regular Bot Method + try: + await ctx.message.channel.fetch_message( + self.repl_sessions[session].id) + except discord.NotFound: + new_shell = await ctx.send(embed=self.repl_embeds[shell]) + self.repl_sessions[session] = new_shell + + embed = self.repl_embeds[shell] + del self.repl_embeds[shell] + self.repl_embeds[new_shell] = embed + + shell = self.repl_sessions[session] + + try: + await response.delete() + except discord.Forbidden: + pass + + if len(self.repl_embeds[shell].fields) >= 7: + self.repl_embeds[shell].remove_field(0) + + if cleaned in ('quit', 'exit', 'exit()'): + self.repl_embeds[shell].color = 16426522 + + if self.repl_embeds[shell].title is not discord.Embed.Empty: + history_string = "History for {}\n\n\n".format( + self.repl_embeds[shell].title) + else: + history_string = "History for latest session\n\n\n" + + for item in history.keys(): + history_string += ">>> {}\n{}\n\n".format( + item, + history[item]) + + haste_url = await self._post_to_hastebin(history_string) + return_msg = "[`Leaving shell session. "\ + "History hosted on hastebin.`]({})".format( + haste_url) + + self.repl_embeds[shell].add_field( + name="`>>> {}`".format(cleaned), + value=return_msg, + inline=False) + + await self.repl_sessions[session].edit( + embed=self.repl_embeds[shell]) + + del self.repl_embeds[shell] + del self.repl_sessions[session] + return + + executor = exec + if cleaned.count('\n') == 0: + # single statement, potentially 'eval' + try: + code = compile(cleaned, '', 'eval') + except SyntaxError: + pass + else: + executor = eval + + if executor is exec: + try: + code = compile(cleaned, '', 'exec') + except SyntaxError as err: + self.repl_embeds[shell].color = 15746887 + + return_msg = self._get_syntax_error(err) + + history[cleaned] = return_msg + + if len(cleaned) > 800: + cleaned = "" + if len(return_msg) > 800: + haste_url = await self._post_to_hastebin(return_msg) + return_msg = "[`SyntaxError too big to be printed. "\ + "Hosted on hastebin.`]({})".format( + haste_url) + + self.repl_embeds[shell].add_field( + name="`>>> {}`".format(cleaned), + value=return_msg, + inline=False) + + await self.repl_sessions[session].edit( + embed=self.repl_embeds[shell]) + continue + + variables['message'] = response + + fmt = None + stdout = io.StringIO() + + try: + with redirect_stdout(stdout): + result = executor(code, variables) + if inspect.isawaitable(result): + result = await result + except Exception as err: + self.repl_embeds[shell].color = 15746887 + value = stdout.getvalue() + fmt = '```py\n{}{}\n```'.format( + value, + traceback.format_exc()) + else: + self.repl_embeds[shell].color = 4437377 + + value = stdout.getvalue() + + if result is not None: + fmt = '```py\n{}{}\n```'.format( + value, + result) + + variables['_'] = result + elif value: + fmt = '```py\n{}\n```'.format(value) + + history[cleaned] = fmt + + if len(cleaned) > 800: + cleaned = "" + + try: + if fmt is not None: + if len(fmt) >= 800: + haste_url = await self._post_to_hastebin(fmt) + self.repl_embeds[shell].add_field( + name="`>>> {}`".format(cleaned), + value="[`Content too big to be printed. " + "Hosted on hastebin.`]({})".format( + haste_url), + inline=False) + + await self.repl_sessions[session].edit( + embed=self.repl_embeds[shell]) + else: + self.repl_embeds[shell].add_field( + name="`>>> {}`".format(cleaned), + value=fmt, + inline=False) + + await self.repl_sessions[session].edit( + embed=self.repl_embeds[shell]) + else: + self.repl_embeds[shell].add_field( + name="`>>> {}`".format(cleaned), + value="`Empty response, assumed successful.`", + inline=False) + + await self.repl_sessions[session].edit( + embed=self.repl_embeds[shell]) + + except discord.Forbidden: + pass + + except discord.HTTPException as err: + error_embed = discord.Embed( + color=15746887, + description='**Error**: _{}_'.format(err)) + await ctx.send(embed=error_embed) + + @repl.command(name='jump', + aliases=['hop', 'pull', 'recenter', 'whereditgo']) + async def repljump(self, ctx): + """Brings the shell back down so you can see it again.""" + + session = ctx.message.channel.id + + if session not in self.repl_sessions: + error_embed = discord.Embed( + color=15746887, + description="**Error**: _No shell running in channel._") + await ctx.send(embed=error_embed) + return + + shell = self.repl_sessions[session] + embed = self.repl_embeds[shell] - # Check for Google captcha - test_search = f'{instance}/search?q=test&format=json&lang=en-US' try: - async with self.request.get(test_search) as resp: - response = await resp.json() - response['results'][0]['content'] - except (aiohttp.ClientError, KeyError, IndexError): - return False + await ctx.message.delete() + except discord.Forbidden: + pass - # Reached if passes all checks - return True + try: + await shell.delete() + except discord.errors.NotFound: + pass + new_shell = await ctx.send(embed=embed) + + self.repl_sessions[session] = new_shell + + del self.repl_embeds[shell] + self.repl_embeds[new_shell] = embed + + @repl.command(name='clear', + aliases=['clean', 'purge', 'cleanup', + 'ohfuckme', 'deletthis']) + async def replclear(self, ctx): + """Clears the fields of the shell and resets the color.""" + + session = ctx.message.channel.id + + if session not in self.repl_sessions: + error_embed = discord.Embed( + color=15746887, + description="**Error**: _No shell running in channel._") + await ctx.send(embed=error_embed) + return + + shell = self.repl_sessions[session] + + self.repl_embeds[shell].color = discord.Color.default() + self.repl_embeds[shell].clear_fields() + + try: + await ctx.message.delete() + except discord.Forbidden: + pass + await shell.edit(embed=self.repl_embeds[shell]) + + @commands.command(name='eval') + async def eval_cmd(self, ctx, *, code: str): + """Evaluates Python code.""" + + if self._eval.get('env') is None: + self._eval['env'] = {} + if self._eval.get('count') is None: + self._eval['count'] = 0 + + codebyspace = code.split(" ") + print(codebyspace) + silent = False + if codebyspace[0] == "--silent" or codebyspace[0] == "-s": + silent = True + codebyspace = codebyspace[1:] + code = " ".join(codebyspace) + + self._eval['env'].update({ + 'self': self.bot, + 'ctx': ctx, + 'message': ctx.message, + 'channel': ctx.message.channel, + 'guild': ctx.message.guild, + 'author': ctx.message.author, + }) + + # let's make this safe to work with + code = code.replace('```py\n', '').replace('```', '').replace('`', '') + + _code = ( + 'async def func(self):\n try:\n{}\n ' + 'finally:\n self._eval[\'env\'].update(locals())').format( + textwrap.indent(code, ' ')) + + before = time.monotonic() + # noinspection PyBroadException + try: + exec(_code, self._eval['env']) + func = self._eval['env']['func'] + output = await func(self) + + if output is not None: + output = repr(output) + except Exception as e: + output = '{}: {}'.format(type(e).__name__, e) + after = time.monotonic() + self._eval['count'] += 1 + count = self._eval['count'] + + code = code.split('\n') + if len(code) == 1: + _in = 'In [{}]: {}'.format(count, code[0]) + else: + _first_line = code[0] + _rest = code[1:] + _rest = '\n'.join(_rest) + _countlen = len(str(count)) + 2 + _rest = textwrap.indent(_rest, '...: ') + _rest = textwrap.indent(_rest, ' ' * _countlen) + _in = 'In [{}]: {}\n{}'.format(count, _first_line, _rest) + + message = '```py\n{}'.format(_in) + if output is not None: + message += '\nOut[{}]: {}'.format(count, output) + ms = int(round((after - before) * 1000)) + if ms > 100: # noticeable delay + message += '\n# {} ms\n```'.format(ms) + else: + message += '\n```' + + try: + if ctx.author.id == self.bot.user.id: + await ctx.message.edit(content=message) + else: + if not silent: + await ctx.send(message) + except discord.HTTPException: + if not silent: + with aiohttp.ClientSession() as sesh: + async with sesh.post( + "https://hastebin.com/documents/", + data=output, + headers={"Content-Type": "text/plain"}) as r: + r = await r.json() + embed = discord.Embed( + description=( + "[View output - click]" + "(https://hastebin.com/raw/{})").format( + r["key"])) + await ctx.send(embed=embed) + + @commands.command(aliases=['sys', 'sh']) + async def system(self, ctx, *, command: str): + """Runs system commands.""" + + message = await ctx.send(' Processing...') + result = [] + try: + process = subprocess.Popen(command.split( + ' '), stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = process.communicate() + except FileNotFoundError: + stderr = f'Command not found: {command}' + embed = discord.Embed( + title="Command output" + ) + if len(result) >= 1 and result[0] in [None, b'']: + stdout = 'No output.' + if len(result) >= 2 and result[0] in [None, b'']: + stderr = 'No output.' + if len(result) >= 1 and result[0] not in [None, b'']: + stdout = result[0].decode('utf-8') + if len(result) >= 2 and result[1] not in [None, b'']: + stderr = result[1].decode('utf-8') + string = "" + if len(result) >= 1: + if (len(result[0]) >= 1024): + stdout = result[0].decode('utf-8') + string = string + f'[[STDOUT]]\n{stdout}' + link = await self._post_to_hastebin(string) + await message.edit( + content=f":x: Content too long. {link}", + embed=None) + return + if len(result) >= 2: + if (len(result[1]) >= 1024): + stdout = result[0].decode('utf-8') + string = string + f'[[STDERR]]\n{stdout}' + link = await self._post_to_hastebin(string) + await message.edit( + content=f":x: Content too long. {link}", + embed=None) + return + embed.add_field( + name="stdout", + value=f'```{stdout}```' if 'stdout' in locals() else 'No output.', + inline=False) + embed.add_field( + name="stderr", + value=f'```{stderr}```' if 'stderr' in locals() else 'No output.', + inline=False) + await message.edit(content='', embed=embed) + + # @commands.command() + # async def git(self, ctx, sub, flags=""): + # """Runs some git commands in Discord.""" + + # if sub == "gud": + # if not flags: + # return await ctx.send("```You are now so gud!```") + # else: + # return await ctx.send( + # "```{} is now so gud!```".format(flags)) + # elif sub == "rekt": + # if not flags: + # return await ctx.send("```You got #rekt!```") + # else: + # return await ctx.send( + # "```{} got #rekt!```".format(flags)) + # else: + # process_msg = await ctx.send( + # " Processing...") + # process = subprocess.Popen( + # f"git {sub + flags}", + # stdout=subprocess.PIPE, + # stderr=subprocess.PIPE) + # res = process.communicate() + # if res[0] == b'': + # content = "Successful!" + # else: + # content = res[0].decode("utf8") + # return await process_msg.edit(content=f"```{content}```") @commands.command() - async def rejson(self, ctx): - '''Refreshes the list of instances for searx.''' - - msg = await ctx.send(' Refreshing instance list...\n\n' - '(Due to extensive quality checks, this may take a bit.)') - plausible = [] - - # Get, parse, and quality check all instances - async with self.request.get('https://searx.space/data/instances.json') as r: - # Parsing - searx_json = await r.json() - instances = searx_json['instances'] - - # Quality Check - for i in instances: - info = instances.get(i) - is_good = await self._instance_check(i, info) - if is_good: - plausible.append(i) - - # Save new list - self.instances = plausible - with open('searxes.txt', 'w') as f: - f.write('\n'.join(plausible)) - - await msg.edit(content='Instances refreshed!') - - @commands.command(aliases=['exit', 'reboot']) - async def restart(self, ctx): - await ctx.send(':zzz: **Restarting.**') - exit() - -# @commands.command() -# async def stats(self, ctx): -# mem = psutil.virtual_memory() -# currproc = psutil.Process(os.getpid()) -# total_ram = self.humanbytes(mem[0]) -# available_ram = self.humanbytes(mem[1]) -# usage = self.humanbytes(currproc.memory_info().rss) -# text = f""" -# ``` -# Total RAM: {total_ram} -# Available RAM: {available_ram} -# RAM used by bot: {usage} -# Number of bot commands: {len(ctx.bot.commands)} -# Number of extensions present: {len(ctx.bot.cogs)} -# Number of users: {len(ctx.bot.users)} -# ``` -# """ -# await ctx.send(text) - - @commands.command(hidden=True) async def error(self, ctx): + """Makes the bot error out.""" + 3 / 0 async def cog_check(self, ctx): - return (ctx.author.id == self.bot.appinfo.owner.id) + def setup(bot): - bot.add_cog(Developer(bot)) \ No newline at end of file + bot.add_cog(Developer(bot)) diff --git a/extensions/search.py b/extensions/search.py index 38e4bfe..094ff28 100644 --- a/extensions/search.py +++ b/extensions/search.py @@ -11,8 +11,9 @@ import aiohttp import random import sys + class Search(commands.Cog): - def __init__(self, bot): + def __init__(self, bot): self.bot = bot self.request = bot.request self.instances = bot.instances @@ -52,8 +53,8 @@ class Search(commands.Cog): # Error Template error_msg = ("**An error occured!**\n\n" - f"There was a problem with `{instance}`. Please try again later.\n" - f"_If problems with this instance persist, contact`{self.bot.appinfo.owner}` to have it removed._") + f"There was a problem with `{instance}`. Please try again later.\n" + f"_If problems with this instance persist, contact`{self.bot.appinfo.owner}` to have it removed._") # Create the URL to make an API call to call = f'{instance}/search?q={query}&format=json&language=en-US' @@ -103,17 +104,54 @@ class Search(commands.Cog): [f"**{entry['title']}** <{entry['url']}>" for entry in results[1:5]]) # Instance Info msg += f"\n\n_Results retrieved from instance `{instance}`._" - + # Reached if error with returned results except (KeyError, IndexError) as e: # Logging print(f"{e} with instance {instance}, trying again.") - self.instances.remove(instance) # Weed the instance out - return await self._search_logic(query, is_nsfw) # Recurse until good response + self.instances.remove(instance) # Weed the instance out + # Recurse until good response + return await self._search_logic(query, is_nsfw) return msg - + + async def _instance_check(self, instance, info): + '''Checks the quality of an instance.''' + + # Makes sure proper values exist + if 'error' in info: + return False + if not ('engines' in info and 'initial' in info['timing']): + return False + if not ('google' in info['engines'] and 'enabled' in info['engines']['google']): + return False + + # Makes sure google is enabled + if not info['engines']['google']['enabled']: + return False + + # Makes sure is not Tor + if info['network_type'] != 'normal': + return False + + # Only picks instances that are fast enough + timing = int(info['timing']['initial']) + if timing > 0.20: + return False + + # Check for Google captcha + test_search = f'{instance}/search?q=test&format=json&lang=en-US' + try: + async with self.request.get(test_search) as resp: + response = await resp.json() + response['results'][0]['content'] + except (aiohttp.ClientError, KeyError, IndexError): + return False + + # Reached if passes all checks + return True + @commands.command() async def search(self, ctx, *, query: str): """Search online for results.""" @@ -126,12 +164,41 @@ class Search(commands.Cog): msg = await self._search_logic(query, ctx.channel.is_nsfw()) await ctx.send(msg) + @commands.command() + @commands.is_owner() + async def rejson(self, ctx): + '''Refreshes the list of instances for searx.''' + + msg = await ctx.send(' Refreshing instance list...\n\n' + '(Due to extensive quality checks, this may take a bit.)') + plausible = [] + + # Get, parse, and quality check all instances + async with self.request.get('https://searx.space/data/instances.json') as r: + # Parsing + searx_json = await r.json() + instances = searx_json['instances'] + + # Quality Check + for i in instances: + info = instances.get(i) + is_good = await self._instance_check(i, info) + if is_good: + plausible.append(i) + + # Save new list + self.instances = plausible + with open('searxes.txt', 'w') as f: + f.write('\n'.join(plausible)) + + await msg.edit(content='Instances refreshed!') + @commands.Cog.listener() async def on_command_error(self, ctx, error): """Listener makes no command fallback to searching.""" if isinstance(error, commands.CommandNotFound) or \ - isinstance(error, commands.CheckFailure): + isinstance(error, commands.CheckFailure): # Logging print(f"\n\nNEW CALL: {ctx.author} from {ctx.guild}.\n") @@ -147,4 +214,4 @@ class Search(commands.Cog): def setup(bot): - bot.add_cog(Search(bot)) \ No newline at end of file + bot.add_cog(Search(bot)) diff --git a/main.py b/main.py index 0441caf..75d804b 100644 --- a/main.py +++ b/main.py @@ -21,7 +21,7 @@ class Bot(commands.Bot): '''Custom Bot Class that overrides the commands.ext one''' def __init__(self, **options): - super().__init__(self.get_prefix_new, **options) + super().__init__(self._get_prefix_new, **options) print('Performing initialization...\n') # Get Config Values @@ -37,7 +37,9 @@ class Bot(commands.Bot): print('Initialization complete.\n\n') - async def get_prefix_new(self, bot, msg): + async def _get_prefix_new(self, bot, msg): + '''Full flexible check for prefix.''' + if isinstance(msg.channel, discord.DMChannel) and self.config['PREFIXLESS_DMS']: plus_none = self.prefix.copy() plus_none.append('') @@ -45,18 +47,11 @@ class Bot(commands.Bot): else: return commands.when_mentioned_or(*self.prefix)(bot, msg) - def init_extensions(self): - for ext in os.listdir('extensions'): - if ext.endswith('.py'): - self.load_extension(f'extensions.{ext[:-3]}') - - async def start(self, *args, **kwargs): - async with aiohttp.ClientSession() as self.request: - self.init_extensions() - await super().start(*args, **kwargs) - async def on_ready(self): + self.request = aiohttp.ClientSession() self.appinfo = await self.application_info() + # EXTENSION ENTRY POINT + self.load_extension('extensions.core') msg = "CONNECTED!\n" msg += "-----------------------------\n" @@ -92,24 +87,8 @@ bot = Bot( case_insensitive=True) -@bot.command(aliases=['info', 'source', 'server']) -async def about(ctx): - '''Returns information about this bot.''' - appinfo = await bot.application_info() - - msg = f"**{bot.description}**\n" - msg += f"Created by **taciturasa#4365**, this instance by **{appinfo.owner}.**\n\n" - msg += "**Source Code:** __\n" - msg += "**Support Server:** __\n" - msg += "_Note: Please attempt to contact the hoster of any separate instances before this server._\n\n" - msg += f"_See **{ctx.prefix}** `help` for help, and `stats` for statistics._" - - await ctx.send(msg) - - @bot.listen() async def on_command_error(ctx, error): - if isinstance(error, commands.CommandNotFound): return elif isinstance(error, commands.CommandInvokeError):