From e9e6d04d9d277490de2952348e5017e83210bef3 Mon Sep 17 00:00:00 2001 From: Chovin Date: Mon, 6 Mar 2017 12:25:52 +1000 Subject: [PATCH 01/10] [lolz] WIP selfbot needs testing --- lolz/lolz.py | 118 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 31 deletions(-) diff --git a/lolz/lolz.py b/lolz/lolz.py index 34a87fa..4161b25 100644 --- a/lolz/lolz.py +++ b/lolz/lolz.py @@ -15,7 +15,7 @@ from discord.ext import commands import discord.utils -from .utils.dataIO import fileIO +from .utils.dataIO import fileIO, dataIO import logging import asyncio import re @@ -28,7 +28,9 @@ log = logging.getLogger(__name__) request_logging_format = '{method} {response.url} has returned {response.status}' request_success_log = '{response.url} with {json} received {data}' - +mvs = '᠎' +nbs = '' +LOLZ_PREFIX = nbs + mvs + nbs default_settings = {"SERVER": {"DEFAULT": False}, "DM": {"DEFAULT" : False}} @@ -102,19 +104,10 @@ async def predicate(destination, content=None, *args, **kwargs): content = content and str(content) # replaced _resolve_destination. assume not getting Object - channel = self.bot.get_channel(destination.id) - # channel should be PrivateChannel, Channel, or None here. - is_private = not hasattr(channel, 'is_private') or channel.is_private - server_on = not is_private and self.settings["SERVER"].get(channel.server.id, False) - dm_on = (channel and is_private and self.settings["DM"].get(channel.user.id, False) - ) or (channel is None and self.settings["DM"].get(destination.id, False)) - - if server_on or dm_on: - if content: - # if not a link -- moved to sentence - content = self.translate_sentence(content) - if embed: - self.in_place_translate_embed(embed) + chan_or_id = self.bot.get_channel(destination.id) or destination.id + # channel should be PrivateChannel, Channel, or id here. + if self.can_lolz(chan_or_id): + self.translate_message(content, embed) # msg = await old_send(self.bot, destination, content, *args, # **kwargs) @@ -122,9 +115,45 @@ async def predicate(destination, content=None, *args, **kwargs): return msg predicate.old = old_send + return predicate + + def edit_lolz(self, old_edit): + async def predicate(message, new_content=None, *args, **kwargs): + embed = kwargs.get('embed', None) + content = new_content and str(new_content) + + chan_or_id = message.channel or message.author.id + + if self.can_lolz(chan_or_id): + self.translate_message(content, embed) + msg = await old_edit(message, content, *args, **kwargs) + return msg + + predicate.old = old_edit return predicate + def translate_message(self, content=None, embed=None): + if content: + # if not a link -- moved to sentence + content = self.translate_sentence(content) + if embed: + self.in_place_translate_embed(embed) + content = LOLZ_PREFIX + (content or '') # for tracking + + def can_lolz(self, chan_or_id): + is_private = getattr(chan_or_id, 'is_private', True) + server_on = (not is_private and + self.settings["SERVER"].get(chan_or_id.server.id, False)) + + if isinstance(chan_or_id, str): + dm_on = self.settings["DM"].get(chan_or_id, False) + else: + dm_on = (is_private and + self.settings["DM"].get(chan_or_id.user.id, False)) + + return server_on and dm_on + def in_place_translate_embed(self, embed): # not sure what provider is # assuming patterns. thought it'd make it easier to maintain. @@ -232,33 +261,60 @@ def translate_sentence(self, sentence): async def patcher(self): await self.bot.wait_until_ready() try: - await asyncio.sleep(6) # be safe lolz + await asyncio.sleep(5) # be safe lolz while True: + await asyncio.sleep(1) if not hasattr(self.bot.send_message, 'old'): - print( - '[WARNING:] -- Overwriting bot.send_message with ' - 'send_lolz. If bot.send_message is not reloaded,') - print( - '[WARNING:] -- in the event of a crash of the lolz ' - 'cog, you may not be able revert to bot.send_message ' - 'without a restart/reloading lolz') + self.prompt('WARNING', 'send_message') self.bot.send_message = self.send_lolz(self.bot.send_message) - await asyncio.sleep(1) + + if self.bot.user.bot and not hasattr(self.bot.edit_message, 'old'): + self.prompt('WARNING', 'edit_message') + self.bot.edit_message = self.edit_lolz(self.bot.edit_message) except asyncio.CancelledError: pass + def prompt(self, option, func_name): + if option == 'WARNING': + print('[WARNING:] -- Overwriting bot.{} with send_lolz. ' + 'If bot.{} is not reloaded,'.format(func_name)) + print('[WARNING:] -- in the event of a crash of the lolz ' + 'cog, you may not be able revert to bot.{} ' + 'without a restart/reloading lolz'.format(func_name)) + elif option == 'TRY_CLEANUP': + print('[CLEANUP:] -- Trying to reload old bot.'.format(func_name)) + else: + print('[CLEANUP:] -- Done. Reloading should have been successful ' + 'unless the lolz cog crashed without cleaning up') + print('[CLEANUP:] -- If that is the case, the bot may need to be ' + 'restarted') + def __unload(self): self._monkeymanager.cancel() # revert any changes done with this method. we should check instead for only lolz override if hasattr(self.bot.send_message, 'old'): - print('[CLEANUP:] -- Trying to reload old self.bot.send_message') + self.prompt('TRY_CLEANUP', 'send_message') self.bot.send_message = self.bot.send_message.old - print( - '[CLEANUP:] -- Done. Reloading should have been successful ' - 'unless the lolz cog crashed without cleaning up') - print( - '[CLEANUP:] -- If that is the case, the bot may need to be ' - 'restarted') + if hasattr(self.bot.edit_message, 'old'): + self.prompt('TRY_CLEANUP', 'edit_message') + self.bot.edit_message = self.bot.edit_message.old + self.prompt('DONE', 'send_message') + + async def on_message(self, message): + chan_or_id = message.channel or message.author.id + + if not self.bot.user.bot: + return + if self.bot.user != message.author: + return + if not self.can_lolz(chan_or_id): + return + if message.content.startswith(LOLZ_PREFIX): + return + + await self.bot.edit_message(message, + new_content=message.content, + embed=message.embed) def check_mod(ctx): From f29cd02909b16eb8bac18bed29c804fdc7f0e4c1 Mon Sep 17 00:00:00 2001 From: Chovin Date: Mon, 6 Mar 2017 12:34:36 +1000 Subject: [PATCH 02/10] [lolz] hotfix can_lolz --- lolz/lolz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lolz/lolz.py b/lolz/lolz.py index 4161b25..eee0c34 100644 --- a/lolz/lolz.py +++ b/lolz/lolz.py @@ -152,7 +152,7 @@ def can_lolz(self, chan_or_id): dm_on = (is_private and self.settings["DM"].get(chan_or_id.user.id, False)) - return server_on and dm_on + return server_on or dm_on def in_place_translate_embed(self, embed): # not sure what provider is From 1afeee5a8459ed41d419cc9a22630986ef6acafb Mon Sep 17 00:00:00 2001 From: Chovin Date: Mon, 6 Mar 2017 13:11:40 +1000 Subject: [PATCH 03/10] [lolz] hotfix various oversights --- lolz/lolz.py | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/lolz/lolz.py b/lolz/lolz.py index eee0c34..34778f6 100644 --- a/lolz/lolz.py +++ b/lolz/lolz.py @@ -107,7 +107,7 @@ async def predicate(destination, content=None, *args, **kwargs): chan_or_id = self.bot.get_channel(destination.id) or destination.id # channel should be PrivateChannel, Channel, or id here. if self.can_lolz(chan_or_id): - self.translate_message(content, embed) + content, embed = self.translate_message(content, embed) # msg = await old_send(self.bot, destination, content, *args, # **kwargs) @@ -125,7 +125,7 @@ async def predicate(message, new_content=None, *args, **kwargs): chan_or_id = message.channel or message.author.id if self.can_lolz(chan_or_id): - self.translate_message(content, embed) + content, embed = self.translate_message(content, embed) msg = await old_edit(message, content, *args, **kwargs) return msg @@ -142,15 +142,18 @@ def translate_message(self, content=None, embed=None): content = LOLZ_PREFIX + (content or '') # for tracking def can_lolz(self, chan_or_id): + # channel should be PrivateChannel, Channel, or id. + if isinstance(chan_or_id, str): + return self.settings["DM"].get(chan_or_id, False) + + # if no is_private, it should be private. + # might not be relevant with the above handling strings is_private = getattr(chan_or_id, 'is_private', True) server_on = (not is_private and self.settings["SERVER"].get(chan_or_id.server.id, False)) - if isinstance(chan_or_id, str): - dm_on = self.settings["DM"].get(chan_or_id, False) - else: - dm_on = (is_private and - self.settings["DM"].get(chan_or_id.user.id, False)) + dm_on = (is_private and + self.settings["DM"].get(chan_or_id.user.id, False)) return server_on or dm_on @@ -265,24 +268,23 @@ async def patcher(self): while True: await asyncio.sleep(1) if not hasattr(self.bot.send_message, 'old'): - self.prompt('WARNING', 'send_message') + self.info_prompt('WARNING', 'send_message') self.bot.send_message = self.send_lolz(self.bot.send_message) - - if self.bot.user.bot and not hasattr(self.bot.edit_message, 'old'): - self.prompt('WARNING', 'edit_message') + if not self.bot.user.bot and not hasattr(self.bot.edit_message, 'old'): + self.info_prompt('WARNING', 'edit_message') self.bot.edit_message = self.edit_lolz(self.bot.edit_message) except asyncio.CancelledError: pass - def prompt(self, option, func_name): + def info_prompt(self, option, func_name): if option == 'WARNING': - print('[WARNING:] -- Overwriting bot.{} with send_lolz. ' - 'If bot.{} is not reloaded,'.format(func_name)) + print('[WARNING:] -- Overwriting bot.{0} with send_lolz. ' + 'If bot.{0} is not reloaded,'.format(func_name)) print('[WARNING:] -- in the event of a crash of the lolz ' 'cog, you may not be able revert to bot.{} ' 'without a restart/reloading lolz'.format(func_name)) elif option == 'TRY_CLEANUP': - print('[CLEANUP:] -- Trying to reload old bot.'.format(func_name)) + print('[CLEANUP:] -- Trying to reload old bot.{}'.format(func_name)) else: print('[CLEANUP:] -- Done. Reloading should have been successful ' 'unless the lolz cog crashed without cleaning up') @@ -293,12 +295,12 @@ def __unload(self): self._monkeymanager.cancel() # revert any changes done with this method. we should check instead for only lolz override if hasattr(self.bot.send_message, 'old'): - self.prompt('TRY_CLEANUP', 'send_message') + self.info_prompt('TRY_CLEANUP', 'send_message') self.bot.send_message = self.bot.send_message.old if hasattr(self.bot.edit_message, 'old'): - self.prompt('TRY_CLEANUP', 'edit_message') + self.info_prompt('TRY_CLEANUP', 'edit_message') self.bot.edit_message = self.bot.edit_message.old - self.prompt('DONE', 'send_message') + self.info_prompt('DONE', 'send_message') async def on_message(self, message): chan_or_id = message.channel or message.author.id @@ -312,9 +314,11 @@ async def on_message(self, message): if message.content.startswith(LOLZ_PREFIX): return + embed = message.embeds[0] if len(message.embeds) else None + await self.bot.edit_message(message, new_content=message.content, - embed=message.embed) + embed=embed) def check_mod(ctx): From 373bcef7cc2bb81a72cee8342721c262438f7218 Mon Sep 17 00:00:00 2001 From: Chovin Date: Mon, 6 Mar 2017 13:12:39 +1000 Subject: [PATCH 04/10] [lolz] moving stuff around --- lolz/lolz.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/lolz/lolz.py b/lolz/lolz.py index 34778f6..0ff0a40 100644 --- a/lolz/lolz.py +++ b/lolz/lolz.py @@ -133,14 +133,6 @@ async def predicate(message, new_content=None, *args, **kwargs): predicate.old = old_edit return predicate - def translate_message(self, content=None, embed=None): - if content: - # if not a link -- moved to sentence - content = self.translate_sentence(content) - if embed: - self.in_place_translate_embed(embed) - content = LOLZ_PREFIX + (content or '') # for tracking - def can_lolz(self, chan_or_id): # channel should be PrivateChannel, Channel, or id. if isinstance(chan_or_id, str): @@ -157,6 +149,15 @@ def can_lolz(self, chan_or_id): return server_on or dm_on + def translate_message(self, content=None, embed=None): + if content: + # if not a link -- moved to sentence + content = self.translate_sentence(content) + if embed: + self.in_place_translate_embed(embed) + content = LOLZ_PREFIX + (content or '') # for tracking + return content, embed + def in_place_translate_embed(self, embed): # not sure what provider is # assuming patterns. thought it'd make it easier to maintain. @@ -276,6 +277,17 @@ async def patcher(self): except asyncio.CancelledError: pass + def __unload(self): + self._monkeymanager.cancel() + # revert any changes done with this method. we should check instead for only lolz override + if hasattr(self.bot.send_message, 'old'): + self.info_prompt('TRY_CLEANUP', 'send_message') + self.bot.send_message = self.bot.send_message.old + if hasattr(self.bot.edit_message, 'old'): + self.info_prompt('TRY_CLEANUP', 'edit_message') + self.bot.edit_message = self.bot.edit_message.old + self.info_prompt('DONE', 'send_message') + def info_prompt(self, option, func_name): if option == 'WARNING': print('[WARNING:] -- Overwriting bot.{0} with send_lolz. ' @@ -291,17 +303,6 @@ def info_prompt(self, option, func_name): print('[CLEANUP:] -- If that is the case, the bot may need to be ' 'restarted') - def __unload(self): - self._monkeymanager.cancel() - # revert any changes done with this method. we should check instead for only lolz override - if hasattr(self.bot.send_message, 'old'): - self.info_prompt('TRY_CLEANUP', 'send_message') - self.bot.send_message = self.bot.send_message.old - if hasattr(self.bot.edit_message, 'old'): - self.info_prompt('TRY_CLEANUP', 'edit_message') - self.bot.edit_message = self.bot.edit_message.old - self.info_prompt('DONE', 'send_message') - async def on_message(self, message): chan_or_id = message.channel or message.author.id From 4cb25f3e118ae3204aec784d4e25c98ad21ccd86 Mon Sep 17 00:00:00 2001 From: Chovin Date: Mon, 6 Mar 2017 22:57:21 +1000 Subject: [PATCH 05/10] [lolz] debug for testing --- lolz/lolz.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lolz/lolz.py b/lolz/lolz.py index 0ff0a40..d48c539 100644 --- a/lolz/lolz.py +++ b/lolz/lolz.py @@ -307,12 +307,16 @@ async def on_message(self, message): chan_or_id = message.channel or message.author.id if not self.bot.user.bot: + print("{}: not a selfbot".format(message.author)) return if self.bot.user != message.author: + print("{}: not the author".format(message.author)) return if not self.can_lolz(chan_or_id): + print("{}: can't lolz".format(message.channel)) return if message.content.startswith(LOLZ_PREFIX): + print("{}: already lolzed".format(message.content)) return embed = message.embeds[0] if len(message.embeds) else None From e7761a472b7cc4bfb2f3fd08db6430cee192cbd5 Mon Sep 17 00:00:00 2001 From: Chovin Date: Mon, 6 Mar 2017 23:18:30 +1000 Subject: [PATCH 06/10] [lolz] lolz selfbot msgs --- lolz/lolz.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lolz/lolz.py b/lolz/lolz.py index d48c539..a9e3db8 100644 --- a/lolz/lolz.py +++ b/lolz/lolz.py @@ -306,17 +306,13 @@ def info_prompt(self, option, func_name): async def on_message(self, message): chan_or_id = message.channel or message.author.id - if not self.bot.user.bot: - print("{}: not a selfbot".format(message.author)) + if self.bot.user.bot: return if self.bot.user != message.author: - print("{}: not the author".format(message.author)) return if not self.can_lolz(chan_or_id): - print("{}: can't lolz".format(message.channel)) return if message.content.startswith(LOLZ_PREFIX): - print("{}: already lolzed".format(message.content)) return embed = message.embeds[0] if len(message.embeds) else None From 0fce07b34f51bf0a96a6d3a74ad25970d2b62605 Mon Sep 17 00:00:00 2001 From: Chovin Date: Wed, 8 Mar 2017 13:26:39 +1000 Subject: [PATCH 07/10] [lolz] debug again :3 --- lolz/lolz.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lolz/lolz.py b/lolz/lolz.py index a9e3db8..6b0a081 100644 --- a/lolz/lolz.py +++ b/lolz/lolz.py @@ -307,12 +307,16 @@ async def on_message(self, message): chan_or_id = message.channel or message.author.id if self.bot.user.bot: + print("{}: not a selfbot".format(message.author)) return if self.bot.user != message.author: + print("{}: not the author".format(message.author)) return if not self.can_lolz(chan_or_id): + print("{}: can't lolz".format(message.channel)) return if message.content.startswith(LOLZ_PREFIX): + print("{}: already lolzed".format(message.content)) return embed = message.embeds[0] if len(message.embeds) else None From c22a3e7fe6f2f979aaa8f305098a5089e92fc93b Mon Sep 17 00:00:00 2001 From: Chovin Date: Wed, 8 Mar 2017 13:41:29 +1000 Subject: [PATCH 08/10] [lolz] selfbot DM fix hopefully --- lolz/lolz.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lolz/lolz.py b/lolz/lolz.py index 6b0a081..89a2156 100644 --- a/lolz/lolz.py +++ b/lolz/lolz.py @@ -75,7 +75,7 @@ async def lolz(self, ctx): key = "SERVER" if server is None or check_mod(ctx): if server is None: - server = ctx.message.author + server = ctx.message.channel or ctx.message.author key = "DM" if server.id not in self.settings[key]: self.settings[key][ @@ -307,16 +307,12 @@ async def on_message(self, message): chan_or_id = message.channel or message.author.id if self.bot.user.bot: - print("{}: not a selfbot".format(message.author)) return if self.bot.user != message.author: - print("{}: not the author".format(message.author)) return if not self.can_lolz(chan_or_id): - print("{}: can't lolz".format(message.channel)) return if message.content.startswith(LOLZ_PREFIX): - print("{}: already lolzed".format(message.content)) return embed = message.embeds[0] if len(message.embeds) else None From 96c51e968a47888d0d2050fa52712cbd23a0f4dd Mon Sep 17 00:00:00 2001 From: Chovin Date: Wed, 8 Mar 2017 21:29:43 +1000 Subject: [PATCH 09/10] [repl] paging to channel single/multiple pages reaction menu (mobile-friendly) --- repl/repl.py | 230 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 218 insertions(+), 12 deletions(-) diff --git a/repl/repl.py b/repl/repl.py index a7309bf..2fb0cdc 100644 --- a/repl/repl.py +++ b/repl/repl.py @@ -11,6 +11,7 @@ from __main__ import send_cmd_help import io, sys, os import subprocess +from collections import OrderedDict # TODO: rtfs # * functionify @@ -22,6 +23,17 @@ # * look in downloads if not found # * mention that it's not installed +class ReactionRemoveEvent(asyncio.Event): + def __init__(self, emojis, author): + super().__init__() + self.emojis = emojis + self.author = author + self.reaction = None + + def set(self, reaction): + self.reaction = reaction + return super().set() + class REPL: def __init__(self, bot): @@ -29,6 +41,7 @@ def __init__(self, bot): self.settings = dataIO.load_json('data/repl/settings.json') self.output_file = "data/repl/temp_output.txt" self.sessions = set() + self.reaction_remove_events = {} def cleanup_code(self, content): """Automatically removes code blocks from the code.""" @@ -47,7 +60,11 @@ async def print_results(self, ctx, results): nbs = '​' discord_fmt = nbs+'```py\n{}\n```' if len(discord_fmt.format(results)) > 2000: - if self.settings["OUTPUT_REDIRECT"] == "pm": + if self.settings["OUTPUT_REDIRECT"] == "pages": + task = self.interactive_results(ctx, results, + single_msg=not self.settings["MULTI_MSG_PAGING"]) + self.bot.loop.create_task(task) + elif self.settings["OUTPUT_REDIRECT"] == "pm": await self.bot.send_message(msg.channel, 'Content too big. Check your PMs') enough_paper = 20 for page in pagify(results, ['\n', ' '], shorten_by=12): @@ -69,6 +86,139 @@ async def print_results(self, ctx, results): else: await self.bot.send_message(msg.channel, discord_fmt.format(results)) + async def interactive_results(self, ctx, results, single_msg=True): + author = ctx.message.author + channel = ctx.message.channel + + if single_msg: + choices = OrderedDict((('◀', 'prev'), + ('❌', 'close'), + ('▶', 'next'))) + else: + choices = OrderedDict((('❌', 'close'), + ('🔽', 'next'))) + + nbs = '​' + discord_fmt = nbs+'```py\n{}\n```' + prompt = (" Output too long. Navigate pages with ({})" + .format('/'.join(choices.values()))) + + pager = pagify(results, ['\n', ' '], page_length=1500) + # results is not a generator, so no reason to keep this as one + pages = [discord_fmt.format(p) + 'pg. {}'.format(c+1) + for c, p in enumerate(pager)] + pages[0] += prompt + + choice = 'next' + page_num = 0 + dirs = {'next': 1, 'prev': -1} + msgs = [] + while choice: + msg = await self.display_page(pages[page_num], channel, choices, + msgs, single_msg) + choice = await self.wait_for_interaction(msg, author, choices) + if choice == 'close': + try: + await self.bot.delete_messages(msgs) + except: # selfbots + for m in msgs: + await self.bot.delete_message(m) + break + if choice in dirs: + page_num = (page_num + dirs[choice]) % len(pages) + if choice is None: + await self.remove_reactions(msgs.pop()) + + async def remove_reactions(self, msg): + channel = msg.channel + botm = msg.server.me + if botm.permissions_in(channel).manage_messages: + await self.bot.clear_reactions(msg) + else: + await asyncio.gather(*(self.bot.remove_reaction(msg, r.emoji, botm) + for r in msg.reactions if r.me), + return_exceptions=True) + async def display_page(self, page, channel, emojis, msgs, overwrite_prev): + if msgs and overwrite_prev: + msg = msgs.pop() + embed = msg.embeds[0] if len(msg.embeds) else None + msg = await self.bot.edit_message(msg, new_content=page, embed=embed) + else: + send_msg = self.bot.send_message(channel, page) + if msgs: + # refresh msg + prv_msg = await self.bot.get_message(channel, msgs[len(msgs)-1].id) + botm = channel.server.me + tasks = (send_msg, self.remove_reactions(prv_msg)) + results = await asyncio.gather(*tasks, return_exceptions=True) + msg = results[0] + else: + msg = await send_msg + try: + for e in emojis: # we want these to be in order + await self.bot.add_reaction(msg, e) + except: + pass + msgs.append(msg) + return msg + + async def wait_for_interaction(self, msg, author, choices: OrderedDict, + timeout=120, delete_msg=True): + """waits for a message or reaction add/remove + if the resonse is a msg, schedules msg deletion it if delete_msg""" + + emojis = tuple(choices.keys()) + words = tuple(choices.values()) + + def mcheck(msg): return msg.content.lower() in words + + tasks = (self.bot.wait_for_message(author=author, timeout=timeout, + channel=msg.channel, check=mcheck), + self.bot.wait_for_reaction(user=author, timeout=timeout, + message=msg, emoji=emojis), + self.wait_for_reaction_remove(user=author, timeout=timeout, + message=msg, emoji=emojis)) + + def msgconv(msg): + res = msg.content.lower() + async def try_del(): + try: + await self.bot.delete_message(msg) + except: + pass + self.bot.loop.create_task(try_del()) + return res + def mojichoice(r): return choices[r.reaction.emoji] + converters = (msgconv, mojichoice, mojichoice) + return await wait_for_first_response(tasks, converters) + + + + async def wait_for_reaction_remove(self, emoji=None, *, user=None, + timeout=None, message=None, check=None): + """Waits for a reaction to be removed by a user from a message within a time period. + Made to act like other discord.py wait_for_* functions but is not fully implemented. + + Because of that, wait_for_reaction_remove(self, emoji: list, user, message, timeout=None) + is a better representation of this function's def + + returns the actual event or None if timeout + """ + if not (emoji and user and message) or check or isinstance(emoji, str): + raise NotImplementedError("wait_for_reaction_remove(self, emoji, " + "user, message, timeout=None) is a better " + "representation of this function definition") + remove_event = ReactionRemoveEvent(emoji, user) + self.reaction_remove_events[message.id] = remove_event + done, pending = await asyncio.wait([remove_event.wait()], + timeout=timeout) + res = self.reaction_remove_events.pop(message.id) + try: + return done.pop().result() and res + except: + return None + + @commands.command(pass_context=True, hidden=True) @checks.is_owner() async def repl(self, ctx): @@ -157,32 +307,87 @@ async def replset(self, ctx): async def replset_print(self, ctx, discord_console_file): """Sets where repl content goes when response is too large. Choices are - pm - console - file + pages - navigable pager in the current + pm - send up to 20 pages via pm + console - print results to console + file - write results to a file, optionally opening in subl/atom """ server = ctx.message.server channel = ctx.message.channel author = ctx.message.author - if discord_console_file not in ['pm','console','file']: + if discord_console_file not in ['pm', 'console', 'file', 'pages']: await self.bot.say('Choices are discord/console/file') return if discord_console_file == 'file': - choices = ['subl','subl.exe','atom','atom.exe'] - await self.bot.say("You chose to print to file. What would you like to open it with?\n" - "Choose between: {}".format(' | '.join(choices+['nothing']))) - answer = await self.bot.wait_for_message(timeout=20, author=author) - answer = answer.content + choices = ['subl', 'subl.exe', 'atom', 'atom.exe'] + msg = ("You chose to print to file. What would you like to open it with?\n" + "Choose between: {}".format(' | '.join(choices+['nothing']))) + answer = await self.user_choice(author, msg, choices) if answer not in choices: - answer = None await self.bot.say("ok, I won't open it after writing to {}".format(self.output_file)) else: await self.bot.say("output will be opened with: {} {}".format(answer,self.output_file)) self.settings['OPEN_CMD'] = answer + elif discord_console_file == 'pages': + choices = ['yes', 'no', 'y', 'n'] + msg = ("Add pages as new messages instead of navigating through the pages " + "by editing one message? (yes/no)") + answer = await self.user_choice(author, msg, choices) + answer = answer is not None and answer[0] == 'y' + if answer: + await self.bot.say("ok, you will be given the option to page via adding new pages") + else: + await self.bot.say("ok, regular single-message paging will be used instead") + self.settings['MULTI_MSG_PAGING'] = answer self.settings["OUTPUT_REDIRECT"] = discord_console_file dataIO.save_json("data/repl/settings.json", self.settings) await self.bot.say("repl overflow will now go to "+discord_console_file) + async def user_choice(self, author, msg, choices, timeout=20): + await self.bot.say(msg) + choices = [c.lower() for c in choices] + answer = await self.bot.wait_for_message(timeout=timeout, + author=author) + answer = answer.content.lower() + return answer if answer in choices else None + + + async def on_reaction_remove(self, reaction, user): + """Handles watching for reactions for wait_for_reaction_remove""" + event = self.reaction_remove_events.get(reaction.message.id, None) + if (event and not event.is_set() and + user == event.author and + reaction.emoji in event.emojis): + event.set(reaction) + + + +async def wait_for_first_response(tasks, converters): + """given a list of unawaited tasks and non-coro result parsers to be called on the results, + this function returns the 1st result that is returned and converted + + if it is possible for 2 tasks to complete at the same time, + only the 1st result deteremined by asyncio.wait will be returned + + returns None if none successfully complete + returns 1st error raised if any occur (probably) + """ + primed = [wait_for_result(t, c) for t, c in zip(tasks, converters)] + done, pending = await asyncio.wait(primed, return_when=asyncio.FIRST_COMPLETED) + for p in pending: + p.cancel() + + try: + return done.pop().result() + except: + return None + + +async def wait_for_result(task, converter): + """await the task call and return its results parsed through the converter""" + # why did I do this? + return converter(await task) + def check_folders(): folder = "data/repl" @@ -192,7 +397,8 @@ def check_folders(): def check_files(): - default = {"OUTPUT_REDIRECT" : "discord", "OPEN_CMD" : None} + default = {"OUTPUT_REDIRECT": "discord", "OPEN_CMD": None, + "MULTI_MSG_PAGING": False} settings_path = "data/repl/settings.json" if not os.path.isfile(settings_path): From 949b993d21f578dd50268af87ddcf6028ecd41c2 Mon Sep 17 00:00:00 2001 From: Chovin Date: Wed, 8 Mar 2017 21:29:43 +1000 Subject: [PATCH 10/10] Revert "[repl] paging to channel" This reverts commit 96c51e968a47888d0d2050fa52712cbd23a0f4dd. --- repl/repl.py | 230 +++------------------------------------------------ 1 file changed, 12 insertions(+), 218 deletions(-) diff --git a/repl/repl.py b/repl/repl.py index 2fb0cdc..a7309bf 100644 --- a/repl/repl.py +++ b/repl/repl.py @@ -11,7 +11,6 @@ from __main__ import send_cmd_help import io, sys, os import subprocess -from collections import OrderedDict # TODO: rtfs # * functionify @@ -23,17 +22,6 @@ # * look in downloads if not found # * mention that it's not installed -class ReactionRemoveEvent(asyncio.Event): - def __init__(self, emojis, author): - super().__init__() - self.emojis = emojis - self.author = author - self.reaction = None - - def set(self, reaction): - self.reaction = reaction - return super().set() - class REPL: def __init__(self, bot): @@ -41,7 +29,6 @@ def __init__(self, bot): self.settings = dataIO.load_json('data/repl/settings.json') self.output_file = "data/repl/temp_output.txt" self.sessions = set() - self.reaction_remove_events = {} def cleanup_code(self, content): """Automatically removes code blocks from the code.""" @@ -60,11 +47,7 @@ async def print_results(self, ctx, results): nbs = '​' discord_fmt = nbs+'```py\n{}\n```' if len(discord_fmt.format(results)) > 2000: - if self.settings["OUTPUT_REDIRECT"] == "pages": - task = self.interactive_results(ctx, results, - single_msg=not self.settings["MULTI_MSG_PAGING"]) - self.bot.loop.create_task(task) - elif self.settings["OUTPUT_REDIRECT"] == "pm": + if self.settings["OUTPUT_REDIRECT"] == "pm": await self.bot.send_message(msg.channel, 'Content too big. Check your PMs') enough_paper = 20 for page in pagify(results, ['\n', ' '], shorten_by=12): @@ -86,139 +69,6 @@ async def print_results(self, ctx, results): else: await self.bot.send_message(msg.channel, discord_fmt.format(results)) - async def interactive_results(self, ctx, results, single_msg=True): - author = ctx.message.author - channel = ctx.message.channel - - if single_msg: - choices = OrderedDict((('◀', 'prev'), - ('❌', 'close'), - ('▶', 'next'))) - else: - choices = OrderedDict((('❌', 'close'), - ('🔽', 'next'))) - - nbs = '​' - discord_fmt = nbs+'```py\n{}\n```' - prompt = (" Output too long. Navigate pages with ({})" - .format('/'.join(choices.values()))) - - pager = pagify(results, ['\n', ' '], page_length=1500) - # results is not a generator, so no reason to keep this as one - pages = [discord_fmt.format(p) + 'pg. {}'.format(c+1) - for c, p in enumerate(pager)] - pages[0] += prompt - - choice = 'next' - page_num = 0 - dirs = {'next': 1, 'prev': -1} - msgs = [] - while choice: - msg = await self.display_page(pages[page_num], channel, choices, - msgs, single_msg) - choice = await self.wait_for_interaction(msg, author, choices) - if choice == 'close': - try: - await self.bot.delete_messages(msgs) - except: # selfbots - for m in msgs: - await self.bot.delete_message(m) - break - if choice in dirs: - page_num = (page_num + dirs[choice]) % len(pages) - if choice is None: - await self.remove_reactions(msgs.pop()) - - async def remove_reactions(self, msg): - channel = msg.channel - botm = msg.server.me - if botm.permissions_in(channel).manage_messages: - await self.bot.clear_reactions(msg) - else: - await asyncio.gather(*(self.bot.remove_reaction(msg, r.emoji, botm) - for r in msg.reactions if r.me), - return_exceptions=True) - async def display_page(self, page, channel, emojis, msgs, overwrite_prev): - if msgs and overwrite_prev: - msg = msgs.pop() - embed = msg.embeds[0] if len(msg.embeds) else None - msg = await self.bot.edit_message(msg, new_content=page, embed=embed) - else: - send_msg = self.bot.send_message(channel, page) - if msgs: - # refresh msg - prv_msg = await self.bot.get_message(channel, msgs[len(msgs)-1].id) - botm = channel.server.me - tasks = (send_msg, self.remove_reactions(prv_msg)) - results = await asyncio.gather(*tasks, return_exceptions=True) - msg = results[0] - else: - msg = await send_msg - try: - for e in emojis: # we want these to be in order - await self.bot.add_reaction(msg, e) - except: - pass - msgs.append(msg) - return msg - - async def wait_for_interaction(self, msg, author, choices: OrderedDict, - timeout=120, delete_msg=True): - """waits for a message or reaction add/remove - if the resonse is a msg, schedules msg deletion it if delete_msg""" - - emojis = tuple(choices.keys()) - words = tuple(choices.values()) - - def mcheck(msg): return msg.content.lower() in words - - tasks = (self.bot.wait_for_message(author=author, timeout=timeout, - channel=msg.channel, check=mcheck), - self.bot.wait_for_reaction(user=author, timeout=timeout, - message=msg, emoji=emojis), - self.wait_for_reaction_remove(user=author, timeout=timeout, - message=msg, emoji=emojis)) - - def msgconv(msg): - res = msg.content.lower() - async def try_del(): - try: - await self.bot.delete_message(msg) - except: - pass - self.bot.loop.create_task(try_del()) - return res - def mojichoice(r): return choices[r.reaction.emoji] - converters = (msgconv, mojichoice, mojichoice) - return await wait_for_first_response(tasks, converters) - - - - async def wait_for_reaction_remove(self, emoji=None, *, user=None, - timeout=None, message=None, check=None): - """Waits for a reaction to be removed by a user from a message within a time period. - Made to act like other discord.py wait_for_* functions but is not fully implemented. - - Because of that, wait_for_reaction_remove(self, emoji: list, user, message, timeout=None) - is a better representation of this function's def - - returns the actual event or None if timeout - """ - if not (emoji and user and message) or check or isinstance(emoji, str): - raise NotImplementedError("wait_for_reaction_remove(self, emoji, " - "user, message, timeout=None) is a better " - "representation of this function definition") - remove_event = ReactionRemoveEvent(emoji, user) - self.reaction_remove_events[message.id] = remove_event - done, pending = await asyncio.wait([remove_event.wait()], - timeout=timeout) - res = self.reaction_remove_events.pop(message.id) - try: - return done.pop().result() and res - except: - return None - - @commands.command(pass_context=True, hidden=True) @checks.is_owner() async def repl(self, ctx): @@ -307,87 +157,32 @@ async def replset(self, ctx): async def replset_print(self, ctx, discord_console_file): """Sets where repl content goes when response is too large. Choices are - pages - navigable pager in the current - pm - send up to 20 pages via pm - console - print results to console - file - write results to a file, optionally opening in subl/atom + pm + console + file """ server = ctx.message.server channel = ctx.message.channel author = ctx.message.author - if discord_console_file not in ['pm', 'console', 'file', 'pages']: + if discord_console_file not in ['pm','console','file']: await self.bot.say('Choices are discord/console/file') return if discord_console_file == 'file': - choices = ['subl', 'subl.exe', 'atom', 'atom.exe'] - msg = ("You chose to print to file. What would you like to open it with?\n" - "Choose between: {}".format(' | '.join(choices+['nothing']))) - answer = await self.user_choice(author, msg, choices) + choices = ['subl','subl.exe','atom','atom.exe'] + await self.bot.say("You chose to print to file. What would you like to open it with?\n" + "Choose between: {}".format(' | '.join(choices+['nothing']))) + answer = await self.bot.wait_for_message(timeout=20, author=author) + answer = answer.content if answer not in choices: + answer = None await self.bot.say("ok, I won't open it after writing to {}".format(self.output_file)) else: await self.bot.say("output will be opened with: {} {}".format(answer,self.output_file)) self.settings['OPEN_CMD'] = answer - elif discord_console_file == 'pages': - choices = ['yes', 'no', 'y', 'n'] - msg = ("Add pages as new messages instead of navigating through the pages " - "by editing one message? (yes/no)") - answer = await self.user_choice(author, msg, choices) - answer = answer is not None and answer[0] == 'y' - if answer: - await self.bot.say("ok, you will be given the option to page via adding new pages") - else: - await self.bot.say("ok, regular single-message paging will be used instead") - self.settings['MULTI_MSG_PAGING'] = answer self.settings["OUTPUT_REDIRECT"] = discord_console_file dataIO.save_json("data/repl/settings.json", self.settings) await self.bot.say("repl overflow will now go to "+discord_console_file) - async def user_choice(self, author, msg, choices, timeout=20): - await self.bot.say(msg) - choices = [c.lower() for c in choices] - answer = await self.bot.wait_for_message(timeout=timeout, - author=author) - answer = answer.content.lower() - return answer if answer in choices else None - - - async def on_reaction_remove(self, reaction, user): - """Handles watching for reactions for wait_for_reaction_remove""" - event = self.reaction_remove_events.get(reaction.message.id, None) - if (event and not event.is_set() and - user == event.author and - reaction.emoji in event.emojis): - event.set(reaction) - - - -async def wait_for_first_response(tasks, converters): - """given a list of unawaited tasks and non-coro result parsers to be called on the results, - this function returns the 1st result that is returned and converted - - if it is possible for 2 tasks to complete at the same time, - only the 1st result deteremined by asyncio.wait will be returned - - returns None if none successfully complete - returns 1st error raised if any occur (probably) - """ - primed = [wait_for_result(t, c) for t, c in zip(tasks, converters)] - done, pending = await asyncio.wait(primed, return_when=asyncio.FIRST_COMPLETED) - for p in pending: - p.cancel() - - try: - return done.pop().result() - except: - return None - - -async def wait_for_result(task, converter): - """await the task call and return its results parsed through the converter""" - # why did I do this? - return converter(await task) - def check_folders(): folder = "data/repl" @@ -397,8 +192,7 @@ def check_folders(): def check_files(): - default = {"OUTPUT_REDIRECT": "discord", "OPEN_CMD": None, - "MULTI_MSG_PAGING": False} + default = {"OUTPUT_REDIRECT" : "discord", "OPEN_CMD" : None} settings_path = "data/repl/settings.json" if not os.path.isfile(settings_path):