async def _url_candidates_from_context(self, ctx: Context): # Can't `yield from` in async functions async for msg in achain([ctx.message], ctx.history(limit=5, before=ctx.message)): for url in _extract_urls_from_message(msg): if await self._is_image(url): yield url
async def addemoji(self, ctx: commands.Context, emoji: str): try: await ctx.message.add_reaction(emoji) except BaseException: await ctx.send(f"<{EMOJIS['XMARK']}> Uh-oh, looks like that emoji doesn't work!") return channel = session.query(Channel).filter(Channel.channel_id == ctx.channel.id).first() if not channel or not channel.poll_channel: msg = f"<{EMOJIS['XMARK']}> This channel isn't setup as a poll channel! Please use `!togglepoll` to enable the feature!" await ctx.send(msg) return channel_emoji = session.query(ChannelEmoji).filter( and_( ChannelEmoji.channel_id == ctx.channel.id, ChannelEmoji.emoji == emoji)).first() if not channel_emoji: channel_emoji = ChannelEmoji(channel_id=ctx.channel.id, emoji=emoji) session.add(channel_emoji) session.commit() await ctx.send(f"<{EMOJIS['CHECK']}> Successfully added {emoji} to the list of emojis to add in this channel!", delete_after=3) async for msg in ctx.history(limit=None): await msg.add_reaction(emoji) else: await ctx.send(f"<{EMOJIS['XMARK']}> That emoji is already set for this channel!")
async def owo(self, ctx: commands.Context): """ Small. """ async for msg in ctx.history(limit=1, before=ctx.message): await ctx.message.delete() owomsg = owo.owoify(msg.content) if len(msg.embeds) > 0: embeds = msg.embeds for embed in embeds: if embed.title: embed.title = owo.owoify(embed.title) if embed.description: embed.description = owo.owoify(embed.description) if embed.footer: embed.set_footer(text=owo.owoify(embed.footer.text), icon_url=embed.footer.icon_url) if embed.author: embed.set_author(name=owo.owoify(embed.author.name), icon_url=embed.footer.icon_url) for f in range(len(embed.fields)): embed.set_field_at( f, name=owo.owoify(embed.fields[f].name), value=owo.owoify(embed.fields[f].value)) await ctx.send(owomsg, embed=embeds[0], allowed_mentions=AllowedMentions.none()) if len(embeds) > 1: for e in embeds[1:]: await ctx.send(embed=e, allowed_mentions=AllowedMentions.none()) elif owomsg != msg.content: await ctx.send(owomsg, allowed_mentions=AllowedMentions.none())
async def _basic_cleanup_strategy(self, ctx: Context, amount: int) -> dict: count = 0 async for msg in ctx.history(limit=amount, before=ctx.message): if msg.author == ctx.me: await msg.delete() count += 1 return {ctx.me: count}
async def delete(self, ctx: commands.Context, count: int = 1): """delete the last messages of the bot""" await deleteMessage(ctx) n = 0 msgs = [] if count < 1: return # Big count means that the int is probably a message id if count > 100: try: msg = await ctx.fetch_message(count) if msg.author.id in (self.bot.user.id, ctx.author.id): await msg.delete() except discord.NotFound: pass return async for message in ctx.history(limit=100): if message.author == self.bot.user: msgs.append(message) n += 1 if n >= count: break try: if not isinstance(ctx.channel, discord.TextChannel): raise TypeError await ctx.channel.delete_messages(msgs) except (discord.errors.Forbidden, AttributeError, TypeError): for msg in msgs: await msg.delete()
async def edit(self, ctx: commands.Context, *args: str): """Edits a map according to the passed arguments""" subm = None if has_map(ctx.message): subm = Submission(ctx.message) elif ctx.message.reference is not None: replied_msg = await ctx.fetch_message( ctx.message.reference.message_id) if has_map(replied_msg): subm = Submission(replied_msg) if subm is None: map_channel = self.get_map_channel(ctx.channel.id) if map_channel is None: return async for msg in ctx.history(): if not has_map(msg): continue by_mapper = str(msg.author.id) in map_channel.mapper_mentions if by_mapper or is_staff( msg.author) or msg.author.id == self.bot.user.id: subm = Submission(msg) break if subm is None: return stdout, file = await subm.edit_map(*args) if stdout: stdout = "```" + stdout + "```" await ctx.channel.send(stdout, file=file)
async def default(self, ctx: commands.Context, param: str) -> discord.Embed: # No idea when this would apply if ctx.message.embeds: return ctx.message.embeds[0] async for message in ctx.history(): if message.embeds: return message.embeds[0] raise commands.MissingRequiredArgument(param)
async def get_nearest(ctx: commands.Context, limit: int = 20, lookup: Callable[[discord.Message], Union[bytes, str]] = get_msg_image, **lookup_kwargs) -> Union[bytes, str]: look = await lookup(ctx.message, **lookup_kwargs) if look is None: async for message in ctx.history(limit=limit): if message.id == ctx.message.id: continue look = await lookup(message, **lookup_kwargs) if look is not None: break return look
async def translate(context: commands.Context, *args: str) -> None: translate_arguments: TranslateArguments = await _parse_translate_arguments( context, args) if translate_arguments.target_member == bot.user: raise BadArgument(f"Can't translate messages sent by the bot.") target_message: discord.Message invocation_message: discord.Message = context.message if invocation_message.reference is not None: # If this message is a reply, the text to translate is the text replied to target_message = invocation_message.reference.resolved # TODO: this has a chance at failing, handle possible exception? if not isinstance(target_message, discord.Message): raise commands.CommandError("Could not extract message from reply") if translate_arguments.target_member is not None: raise BadArgument( "Target member shouldn't be specified on replies") else: # If the invocation isn't a reply, fetch the first valid message from the channel to translate """ There doesn't seem to be a good way to get a member's history within a channel. It would seem that a member's history() method should do it, but this history corresponds to DMs, not to the channel. """ message_in_history: discord.Message async for message_in_history in context.history( limit=_MESSAGE_HISTORY_LIMIT): if (message_in_history.author != bot.user # Ignore messages sent by this bot and context.message != message_in_history # Ignore the message that triggered this invocation and # Just to be safe, ignore all commands that start with the command prefix # TODO: could this check be less broad? (not message_in_history.content.startswith(_COMMAND_PREFIX)) and # Ignore if message author does not match specified target member (if any were given) (translate_arguments.target_member is None or translate_arguments.target_member == message_in_history.author )): target_message = message_in_history break else: raise commands.BadArgument("Found no message to translate.") translated_text: str = translator.translate( target_message.content, target_language=translate_arguments.target_language, source_language=translate_arguments.source_language) await target_message.reply(translated_text)
async def _reactions(self, ctx: Context, search: int = 100) -> None: """Removes all reactions from messages that have them.""" if search > 2000: await ctx.send(f"Too many messages to search for ({search} / 2000)" ) return total_reactions = 0 async for message in ctx.history(limit=search, before=ctx.message): if len(message.reactions): total_reactions += sum(r.count for r in message.reactions) await message.clear_reactions() await ctx.send(f"Successfully removed {total_reactions} reactions.")
async def _find_message(self, ctx: commands.Context, member: discord.Member = None) -> discord.Message: """Finds specified member's last non-command message. If no member was specified, adds emojis to the last non-command message sent in the given channel. """ if member is not None and member != ctx.author: async for message in ctx.history(limit=15): if message.author == member: return message else: messages = await ctx.history(limit=2).flatten() if len(messages) > 1: return messages[1] return None
async def find_image(self, channel: commands.Context, *, sent_by: Optional[discord.Member] = None, limit: int = 15) -> ExtractedImage: attachment, input_image_bytes = None, None async with channel.typing(): async for message in channel.history(limit=limit): if sent_by is not None and message.author != sent_by: continue attachment, input_image_bytes = await self.extract_image( message) if input_image_bytes is not None: break return attachment, input_image_bytes
async def chat_stats(self, context: commands.Context, days: Optional[int]): '''Display stats for this context. Optionally given # days back. Will look only look at most 10k messages Example: !chatstats 10 -> chat stats for the last 10 days ''' async with context.typing(): # at least give some level of feedback counts: Dict[str, int] = {} days_back = datetime.datetime.now() - datetime.timedelta( days=days) if days else None async for message in context.history( limit=10_000, after=days_back, oldest_first=False).filter( lambda m: not m.author.bot): #type: discord.Message counts[message.author.display_name] = counts.get( message.author.display_name, 0) + 1 with plt.xkcd(): fig, axs = plt.subplots() # type: plt.Figure, plt.Axes names = sorted(counts.keys(), key=lambda x: len(x)) values = [counts[n] for n in names] axs.bar(names, values) title = f'Number of messages in #{context.channel.name}' if days_back: axs.set_title(title + f'\nsince {days_back:%Y-%m-%d}') else: axs.set_title(title) # formatting for label in axs.get_xticklabels( ): # type: matplotlib.text.Text label.set_rotation(45) label.set_horizontalalignment('right') fig.tight_layout() buffer = io.BytesIO() fig.savefig(buffer, format='png') buffer.seek(0) await context.send( file=discord.File(buffer, filename='chatstats.png'))
async def snakify_command(self, ctx: Context, *, message: str = None) -> None: """ How would I talk if I were a snake? If `message` is passed, the bot will snakify the message. Otherwise, a random message from the user's history is snakified. Written by Momo and kel. Modified by lemon. """ with ctx.typing(): embed = Embed() user = ctx.author if not message: # Get a random message from the users history messages = [] async for message in ctx.history( limit=500).filter(lambda msg: msg.author == ctx. author # Message was sent by author. ): messages.append(message.content) message = self._get_random_long_message(messages) # Set the avatar if user.avatar is not None: avatar = f"https://cdn.discordapp.com/avatars/{user.id}/{user.avatar}" else: avatar = ctx.author.default_avatar_url # Build and send the embed embed.set_author( name=f"{user.name}#{user.discriminator}", icon_url=avatar, ) embed.description = f"*{self._snakify(message)}*" await ctx.send(embed=embed)
async def default(self, ctx: commands.Context, param: str) -> str: if ctx.message.attachments: return ctx.message.attachments[0].url async for message in ctx.history(): if message.attachments: return message.attachments[0].url if message.embeds: embed = message.embeds[0] if embed.type == "image": if embed.url: return embed.url elif embed.image: return embed.image.url raise commands.MissingRequiredArgument(param)
async def dontbullshit(self, ctx: commands.Context): """Inverts the last 8-Ball answer in the channel.""" async for message in ctx.history(limit=10): if message.author != ctx.me: continue message_content = cast( str, cast(discord.Message, message).clean_content) if not message_content.startswith('🎱 '): continue previous_answer = message_content[2:] if previous_answer in self.ANSWERS['affirmative']: new_category = 'negative' elif previous_answer in self.ANSWERS['negative']: new_category = 'affirmative' else: new_category = random.choice(['affirmative', 'negative']) new_text = f'🎱 {random.choice(self.ANSWERS[new_category])}' await message.edit(content=new_text) break
async def purge_reactions(self, ctx: commands.Context, amount: int = 5): """ Purges reactions in the last <n> messages. """ count = 0 total_reactions_removed = 0 async for message in ctx.history(limit=amount): # no reactions, skip if not message.reactions: continue # calculate total reaction count total_reactions_removed += sum(reaction.count for reaction in message.reactions) # remove all reactions await message.clear_reactions() count += 1 await ctx.send( f'Purge complete. Removed {total_reactions_removed} reaction(s) from {count} message(s).', delete_after=2.5)
async def remoji(self, ctx: commands.Context, emoji: str): member = ctx.guild.get_member(bot.user.id) try: await ctx.message.remove_reaction(emoji, member) except BaseException: await ctx.send(f"<{EMOJIS['XMARK']}> Uh-oh, looks like that emoji doesn't work!") return channel_emoji = session.query(ChannelEmoji).filter( and_( ChannelEmoji.channel_id == ctx.channel.id, ChannelEmoji.emoji == emoji)).first() if channel_emoji: channel_emoji.delete() session.commit() await ctx.send(f"<{EMOJIS['CHECK']}> Successfully removed {emoji} from the list of emojis to add in this channel!", delete_after=3) else: await ctx.send(f"<{EMOJIS['XMARK']}> Couldn't find {emoji} in the list of emojis to add in this channel!") async for msg in ctx.history(limit=None): await msg.remove_reaction(emoji, member)
async def find_image( self, channel: commands.Context, *, sent_by: Optional[discord.Member] = None, message_id: Optional[int] = None, limit: int = 15, ) -> ExtractedImage: attachment, input_image_bytes = None, None if message_id is not None: reference_message = await channel.fetch_message(message_id) attachment, input_image_bytes = await self.extract_image( reference_message) else: async for message in channel.history(limit=limit): if sent_by is not None and message.author != sent_by: continue attachment, input_image_bytes = await self.extract_image( message) if input_image_bytes is not None: break return attachment, input_image_bytes
async def by_amounts( self, ctx: commands.Context, amounts: int = 1, member: Optional[discord.Member] = None, ) -> None: # 辨認觸發指令,以及是否為管理員 if ctx.invoked_parents[ 0] == "clean" or ctx.author.id not in self.moderators: member = ctx.guild.me # 依據是否有指定成員發送確認訊息 if member is None: confirm_msg = await ctx.reply(f"你確定要清除 {amounts} 則 **無指定** 的訊息嗎?") else: confirm_msg = await ctx.reply( f"你確定要清除 {amounts} 則 **{member.display_name}** 的訊息嗎?") await confirm_msg.add_reaction(Reactions.check_mark) await confirm_msg.add_reaction(Reactions.cross_mark) def command_confirm(reaction: discord.Reaction, user: discord.User): if user != self.bot.user: if user == ctx.author and reaction.message.id == confirm_msg.id: if str(reaction.emoji) == Reactions.check_mark: raise ActiveCommand elif str(reaction.emoji) == Reactions.cross_mark: raise CancelCommand try: await self.bot.wait_for("reaction_add", timeout=10, check=command_confirm) # 點選確認 except ActiveCommand: # 清除確認訊息 await confirm_msg.delete() start_time = dt.now() def is_specific(m: discord.Message) -> bool: return member is None or m.author == member history_length = 0 msg_delete_queue = [] async with ctx.typing(): async for m in ctx.history(limit=None, before=ctx.message.created_at, oldest_first=False): history_length += 1 if is_specific(m): msg_delete_queue.append(m) if len(msg_delete_queue) == amounts: break def in_queue(m: discord.Message) -> bool: return m in msg_delete_queue deleted_msg_count = len(await ctx.channel.purge( limit=history_length, before=ctx.message.created_at, oldest_first=False, check=in_queue, )) # 計算花費時間 time_taken = (dt.now() - start_time).total_seconds() h, r = divmod(time_taken, 3600) m, s = divmod(r, 60) # 依據是否有指定成員發送結果 if member is None: await ctx.reply( f"已清除 {deleted_msg_count} 則 **無指定** 的訊息|" f"花費時長:{h:02.0f}:{m:02.0f}:{s:02.0f}", delete_after=15, ) else: await ctx.reply( f"已清除 {deleted_msg_count} 則 **{member.display_name}** 的訊息|" f"花費時長:{h:02.0f}:{m:02.0f}:{s:02.0f}", delete_after=15, ) # 點選取消 except CancelCommand: await confirm_msg.delete() await ctx.reply("指令已取消", delete_after=15) # 未點選 except TimeoutError: await confirm_msg.delete() await ctx.reply("超過等待時間,指令已取消", delete_after=15) finally: await ctx.message.delete(delay=15)