async def ask(bot, channel, author, text, options, timeout=60, show_embed=False, delete_after=False): embed = Embed(color=0x68a910, description='\n'.join(f"{Emoji.get_chat_emoji(option.emoji)} {option.text}" for option in options)) message = await channel.send(text, embed=embed if show_embed else None) handlers = dict() for option in options: emoji = Emoji.get_emoji(option.emoji) await message.add_reaction(emoji) handlers[str(emoji)] = {'handler': option.handler, 'args': option.args} def check(reaction: Reaction, user): return user == author and str(reaction.emoji) in handlers.keys() and reaction.message.id == message.id try: reaction, user = await bot.wait_for('reaction_add', timeout=timeout, check=check) except asyncio.TimeoutError as ex: if delete_after: await message.delete() await channel.send( Lang.get_string("questions/error_reaction_timeout", error_emoji=Emoji.get_emoji("WARNING"), timeout=timeout_format(timeout)), delete_after=10 if delete_after else None) raise ex else: if delete_after: await message.delete() h = handlers[str(reaction.emoji)]['handler'] a = handlers[str(reaction.emoji)]['args'] if h is None: return if inspect.iscoroutinefunction(h): await h(*a) if a is not None else await h() else: h(*a) if a is not None else h()
async def on_message(self, message: discord.Message): try: if message.author.bot\ or not message.attachments\ or not hasattr(message.channel, "guild")\ or message.channel.guild is None\ or message.channel.id != self.channels[message.channel.guild.id][self.listen_tag]: return except KeyError as ex: return ctx = await self.bot.get_context(message) tags = [] for tag in self.channels[ctx.guild.id].keys(): tags.append(f"\\b{re.escape(tag)}\\b") tag_pattern = '|'.join(tags) tag_matcher = re.compile(tag_pattern, re.IGNORECASE) tags = tag_matcher.findall(message.content) if tags: for tag in tags: channel = self.bot.get_channel( self.channels[ctx.guild.id][tag]) for attachment in message.attachments: embed = discord.Embed(timestamp=message.created_at, color=0x663399) embed.add_field(name="Author", value=message.author.mention) embed.add_field(name="Tag", value=f"#{tag}") embed.add_field( name="Jump Link", value=f"[Go to message]({message.jump_url})") embed.add_field(name="URL", value=f"[Download]({attachment.url})") if message.content: embed.add_field(name="Message Content", value=message.content) embed.set_image(url=attachment.url) sent = await channel.send(embed=embed) await sent.add_reaction(Emoji.get_emoji("YES")) await sent.add_reaction(Emoji.get_emoji("NO")) else: channel = self.bot.get_channel( self.channels[ctx.guild.id][self.main_tag]) for attachment in message.attachments: embed = discord.Embed(timestamp=message.created_at, color=0x663399) embed.add_field(name="Author", value=message.author.mention) embed.add_field(name="Jump Link", value=f"[Go to message]({message.jump_url})") embed.add_field(name="URL", value=f"[Download]({attachment.url})") if message.content: embed.add_field(name="Message Content", value=message.content) embed.set_image(url=attachment.url) sent = await channel.send(embed=embed) await sent.add_reaction(Emoji.get_emoji("YES")) await sent.add_reaction(Emoji.get_emoji("NO"))
async def on_raw_reaction_add(self, event): try: channel = self.bot.get_channel(event.channel_id) message = await channel.fetch_message(event.message_id) if event.channel_id not in self.collection_channels[ message.channel.guild.id]: return member = message.channel.guild.get_member(event.user_id) user_is_bot = event.user_id == self.bot.user.id has_permission = member.guild_permissions.mute_members # TODO: change to role-based? if user_is_bot or not has_permission: return await message.clear_reactions() if str(event.emoji) == str(Emoji.get_emoji("NO")): # delete message await message.delete() return except (NotFound, KeyError, AttributeError) as e: # couldn't find channel, message, member, or action return except Exception as e: await Utils.handle_exception("art collector generic exception", self, e) return
async def ask(bot, channel, author, text, options, timeout=60, show_embed=False, delete_after=False, locale="en_US"): description = '\n'.join(f"{Emoji.get_chat_emoji(option.emoji)} {option.text or ''}" for option in options) embed = Embed(color=0x68a910, description=description) message = await channel.send(text, embed=embed if show_embed else None) handlers = dict() for option in options: emoji = Emoji.get_emoji(option.emoji) add_attempts = 10 # try reaction 10x in case it fails to add while add_attempts > 0: try: await message.add_reaction(emoji) break except Exception as ex: add_attempts = add_attempts - 1 handlers[str(emoji)] = {'handler': option.handler, 'args': option.args} def check(reaction: Reaction, user): return user == author and str(reaction.emoji) in handlers.keys() and reaction.message.id == message.id try: reaction, user = await bot.wait_for('reaction_add', timeout=timeout, check=check) except asyncio.TimeoutError as ex: try: if delete_after: await message.delete() await channel.send( Lang.get_locale_string("questions/error_reaction_timeout", locale, error_emoji=Emoji.get_emoji("WARNING"), timeout=timeout_format(timeout)), delete_after=10 if delete_after else None) except Exception as e: # ignore all failures at this point pass raise ex else: if delete_after: await message.delete() h = handlers[str(reaction.emoji)]['handler'] a = handlers[str(reaction.emoji)]['args'] if h is None: return if inspect.iscoroutinefunction(h): await h(*a) if a is not None else await h() else: h(*a) if a is not None else h()
async def do_collect(my_message, my_tag): content_shown = False my_channel = self.bot.get_channel(self.channels[ctx.guild.id][my_message.channel.id][my_tag.lower()]) for attachment in my_message.attachments: embed = discord.Embed( timestamp=my_message.created_at, color=0x663399) embed.add_field(name="Author", value=my_message.author.mention) if my_tag is not self.no_tag: embed.add_field(name="Tag", value=f"#{my_tag}") embed.add_field(name="Jump Link", value=f"[Go to message]({my_message.jump_url})") embed.add_field(name="URL", value=f"[Download]({attachment.url})") if my_message.content and not content_shown: # Add message content to the first of multiples, when many attachments to a single my_message. embed.add_field(name="Message Content", value=my_message.content, inline=False) content_shown = True embed.set_image(url=attachment.url) sent = await my_channel.send(embed=embed) await sent.add_reaction(Emoji.get_emoji("YES")) await sent.add_reaction(Emoji.get_emoji("NO"))
async def add_mod_action(self, trigger, matched, message, response_channel, formatted_response): """ :param message: Trigger message :param response_channel: Channel to respond in :param formatted_response: prepared auto-response :return: None """ embed = discord.Embed( title=f"Trigger: {matched or get_trigger_description(trigger)}", timestamp=message.created_at, color=0xFF0940) embed.add_field(name='Message Author', value=message.author.mention, inline=True) embed.add_field(name='Channel', value=message.channel.mention, inline=True) embed.add_field(name='Jump link', value=f"[Go to message]({message.jump_url})", inline=True) embed.add_field(name='Offending Mesasge', value=f"```{message.content}```", inline=False) embed.add_field(name='Moderator Actions', value=f""" Pass: {Emoji.get_emoji("YES")} Intervene: {Emoji.get_emoji("CANDLE")} Auto-Respond: {Emoji.get_emoji("WARNING")} DESTROY: {Emoji.get_emoji("NO")} """) # message add reactions sent_response = await response_channel.send(embed=embed) await sent_response.add_reaction(Emoji.get_emoji("YES")) await sent_response.add_reaction(Emoji.get_emoji("CANDLE")) await sent_response.add_reaction(Emoji.get_emoji("WARNING")) await sent_response.add_reaction(Emoji.get_emoji("NO")) action = mod_action(message.channel.id, message.id, formatted_response) self.mod_actions[sent_response.id] = action
async def send_bug_info(self, key): channel = self.bot.get_channel(Configuration.get_var("channels")[key]) bug_info_id = Configuration.get_persistent_var(f"{key}_message") if bug_info_id is not None: try: message = await channel.fetch_message(bug_info_id) except NotFound: pass else: await message.delete() if message.id in self.bug_messages: self.bug_messages.remove(message.id) bugemoji = Emoji.get_emoji('BUG') message = await channel.send( Lang.get_string("bugs/bug_info", bug_emoji=bugemoji)) await message.add_reaction(bugemoji) self.bug_messages.add(message.id) Configuration.set_persistent_var(f"{key}_message", message.id)
async def send_bug_info(self, *args): for channel_id in args: channel = self.bot.get_channel(channel_id) if channel is None: await Logging.bot_log(f"can't send bug info to nonexistent channel {channel_id}") continue bug_info_id = Configuration.get_persistent_var(f"{channel.guild.id}_{channel_id}_bug_message") ctx = None tries = 0 while not ctx and tries < 5: tries += 1 # this API call fails on startup because connection is not made yet. # TODO: properly wait for connection to be initialized try: last_message = await channel.send('preparing bug reporting...') ctx = await self.bot.get_context(last_message) if bug_info_id is not None: try: message = await channel.fetch_message(bug_info_id) except (NotFound, HTTPException): pass else: await message.delete() if message.id in self.bug_messages: self.bug_messages.remove(message.id) bugemoji = Emoji.get_emoji('BUG') message = await channel.send(Lang.get_locale_string("bugs/bug_info", ctx, bug_emoji=bugemoji)) self.bug_messages.add(message.id) await message.add_reaction(bugemoji) Configuration.set_persistent_var(f"{channel.guild.id}_{channel_id}_bug_message", message.id) Logging.info(f"Bug report message sent in channel #{channel.name} ({channel.id})") await last_message.delete() except Exception as e: await self.bot.guild_log(channel.guild.id, f'Having trouble sending bug message in {channel.mention}') await Utils.handle_exception( f"Bug report message failed to send in channel #{channel.name} ({channel.id})", self.bot, e) await asyncio.sleep(0.5)
async def on_raw_reaction_add(self, event): """ reaction listener for art collection channels. Clears reactions and on "no" reaction, removes post from collection """ try: m_id = event.message_id u_id = event.user_id g_id = event.guild_id c_id = event.channel_id my_emoji = event.emoji my_member = event.member my_channel = self.bot.get_channel(c_id) my_guild = self.bot.get_guild(g_id) if c_id not in self.collection_channels[my_guild.id]: return user_is_bot = u_id == self.bot.user.id has_permission = my_member.guild_permissions.mute_members # TODO: change to role-based? if user_is_bot or not has_permission: return message = await my_channel.fetch_message(m_id) if str(my_emoji) == str(Emoji.get_emoji("NO")): # delete message await message.delete() return else: await message.clear_reactions() # any reaction will remove the bot reacts except (NotFound, HTTPException, KeyError, AttributeError) as e: # couldn't find channel, message, member, or action return except Exception as e: await Utils.handle_exception("art collector generic exception", self.bot, e) return
async def do_mod_action(self, action, member, message, emoji): """ :param action: namedtuple mod_action to execute :param member: member performing the action :param message: message action is performed on :param emoji: the emoji that was added :return: None """ try: trigger_channel = self.bot.get_channel(action.channel_id) trigger_message = await trigger_channel.fetch_message( action.message_id) except (NotFound, AttributeError) as e: trigger_message = None m = self.bot.metrics if str(emoji) == str(Emoji.get_emoji("YES")): # delete mod action message, leave the triggering message await message.delete() m.auto_responder_mod_pass.inc() return async def update_embed(my_message, mod): # replace mod action list with acting mod name and datetime my_embed = my_message.embeds[0] start = message.created_at react_time = datetime.utcnow() time_d = Utils.to_pretty_time((react_time - start).seconds) nonlocal trigger_message my_embed.set_field_at(-1, name="Handled by", value=mod.mention, inline=True) if trigger_message is None: my_embed.add_field( name="Deleted", value="Member removed message before action was taken.") my_embed.add_field(name="Action Used", value=emoji, inline=True) my_embed.add_field(name="Reaction Time", value=time_d, inline=True) await (my_message.edit(embed=my_embed)) await update_embed(message, member) await message.clear_reactions() await asyncio.sleep(1) if str(emoji) == str(Emoji.get_emoji("CANDLE")): # do nothing m.auto_responder_mod_manual.inc() pass if str(emoji) == str(Emoji.get_emoji("WARNING")): # send auto-response in the triggering channel m.auto_responder_mod_auto.inc() if trigger_message is not None: await trigger_message.channel.send(action.response) if str(emoji) == str(Emoji.get_emoji("NO")): # delete the triggering message m.auto_responder_mod_delete_trigger.inc() if trigger_message is not None: await trigger_message.delete()
async def ask_text( bot, channel, user, text, validator=None, timeout=Configuration.get_var("question_timeout_seconds"), confirm=False, escape=True, delete_after=False, locale="en_US"): def check(msg): return user == msg.author and msg.channel == channel ask_again = True def confirmed(): nonlocal ask_again ask_again = False def clean_text(txt): """Remove multiple spaces and multiple newlines from input txt.""" txt = re.sub(r' +', ' ', txt) txt = re.sub(r'\n\s*\n', '\n\n', txt) return txt my_messages = [] async def clean_dialog(): nonlocal delete_after nonlocal my_messages if delete_after: for msg in my_messages: try: await msg.delete() except Exception as e: pass while ask_again: message_cleaned = "" my_messages.append(await channel.send(text)) try: while True: message = await bot.wait_for('message', timeout=timeout, check=check) my_messages.append(message) if message.content is None or message.content == "": result = Lang.get_locale_string("questions/text_only", locale) else: message_cleaned = clean_text(message.content) result = validator(message_cleaned) if validator is not None else True if result is True: break else: my_messages.append(await channel.send(result)) except asyncio.TimeoutError as ex: await clean_dialog() await channel.send( # TODO: remove "bug" from lang string. send report cancel language from Bugs.py exception handler Lang.get_locale_string("questions/error_reaction_timeout", locale, error_emoji=Emoji.get_emoji("WARNING"), timeout=timeout_format(timeout)) ) raise ex else: content = Utils.escape_markdown(message_cleaned) if escape else message_cleaned if confirm: backticks = "``" if len(message_cleaned.splitlines()) == 1 else "```" message = Lang.get_locale_string('questions/confirm_prompt', locale, backticks=backticks, message=message_cleaned) await ask(bot, channel, user, message, [ Option("YES", handler=confirmed), Option("NO") ], delete_after=delete_after) else: confirmed() await clean_dialog() return content
async def ask_attachements( bot, channel, user, timeout=Configuration.get_var("question_timeout_seconds"), max_files=Configuration.get_var('max_attachments'), locale="en_US"): def check(message): return user == message.author and message.channel == channel done = False def ready(): nonlocal done done = True async def restart_attachments(): nonlocal final_attachments final_attachments = [] await ask(bot, channel, user, Lang.get_locale_string("questions/attachments_restart", locale), [ Option("YES", Lang.get_locale_string('questions/restart_attachments_yes', locale)), Option("NO", Lang.get_locale_string('questions/restart_attachments_no', locale), handler=ready) ], show_embed=True) while not done: ask_again = True final_attachments = [] count = 0 def confirmed(): nonlocal ask_again ask_again = False while ask_again: if not final_attachments: await channel.send(Lang.get_locale_string("questions/attachment_prompt", locale, max=max_files)) elif len(final_attachments) < max_files - 1: await channel.send( Lang.get_locale_string("questions/attachment_prompt_continued", locale, max=max_files - len(final_attachments))) elif len(final_attachments): await channel.send(Lang.get_locale_string("questions/attachment_prompt_final", locale)) done = False try: while True: message = await bot.wait_for('message', timeout=timeout, check=check) links = Utils.URL_MATCHER.findall(message.content) attachment_links = [str(a.url) for a in message.attachments] if len(links) != 0 or len(message.attachments) != 0: if (len(links) + len(message.attachments)) > max_files: await channel.send(Lang.get_locale_string("questions/attachments_overflow", locale, max=max_files)) else: final_attachments += links + attachment_links count += len(links) + len(attachment_links) break else: await channel.send(Lang.get_locale_string("questions/attachment_not_found", locale)) except asyncio.TimeoutError as ex: await channel.send( Lang.get_locale_string("questions/error_reaction_timeout", locale, error_emoji=Emoji.get_emoji("WARNING"), timeout=timeout_format(timeout)) ) raise ex else: if count < max_files: await ask(bot, channel, user, Lang.get_locale_string('questions/another_attachment', locale), [Option("YES"), Option("NO", handler=confirmed)]) else: ask_again = False prompt_yes = Lang.get_locale_string("questions/approve_attachments", locale) if len(final_attachments) == 1: prompt_no = Lang.get_locale_string('questions/restart_attachment_singular', locale) else: prompt_no = Lang.get_locale_string('questions/restart_attachment_plural', locale) await ask(bot, channel, user, Lang.get_locale_string('questions/confirm_attachments', locale), [ Option("YES", prompt_yes, handler=ready), Option("NO", prompt_no, handler=restart_attachments) ], show_embed=True) return final_attachments