async def warn(self, ctx, member: discord.Member, *, reason): """ Warns a member. Reason length is maximum of 200 characters. """ if len(reason) > 200: await ctx.send(embed=failure("Please shorten the reason to 200 characters."), delete_after=3) return embed = infraction_embed(ctx, member, constants.Infraction.warning, reason) embed.add_field( name="**NOTE**", value=( "If you are planning to repeat this again, " "the mods may administer punishment for the action." ) ) try: await self.bot.api_client.add_member_warning(ctx.author.id, member.id, reason) except Exception as e: msg = "Could not apply warning, problem with API." logger.info(f"{msg} {e}") await ctx.send(embed=failure(f"{msg}\nInfraction member should not think he got away.")) else: await self.deterrence_log_channel.send(f"{member.mention}", delete_after=0.5) await self.deterrence_log_channel.send(embed=embed) await ctx.send(embed=success("Warning successfully applied.", ctx.me), delete_after=5) await asyncio.sleep(5) await ctx.message.delete()
async def _get_user_reply(self, container: set, user: discord.User) -> Union[str, None]: """ Helper method to get user reply, only deals with errors. Uses self._wait_for method so it can get both the user message reply and text from attachment file. :param container: set, container holding active user sessions by having their IDs in it. :param user: Discord user to wait reply from :return: Union[str, None] string representing user reply, can be None representing invalid reply. """ user_reply = await self._wait_for(container, user) if user_reply is None: return None try: possible_attachment = await self.get_message_txt_attachment( user_reply) except (UnsupportedFileExtension, UnsupportedFileEncoding) as e: await user.send(embed=failure(f"Error: {e} , canceling.")) container.remove(user.id) return user_reply_content = user_reply.content if possible_attachment is None else possible_attachment if len(user_reply_content) < 10: await user.send( embed=failure("Too short - seems invalid, canceling.")) container.remove(user.id) return None else: return user_reply_content
async def promote(self, ctx, member: discord.Member, role: discord.Role): """ Promote member to role. """ if role >= ctx.author.top_role: await ctx.send( embed=failure("Role needs to be below you in hierarchy.")) return elif role in member.roles: await ctx.send(embed=failure( f"{member.mention} already has role {role.mention}!")) return await member.add_roles(role) await ctx.send(embed=success( f"{member.mention} is promoted to {role.mention}", ctx.me), delete_after=5) dm_embed = info(( f"You are now promoted to role **{role.name}** in our community.\n" f"`'With great power comes great responsibility'`\n" f"Be active and keep the community safe."), ctx.me, "Congratulations!") dm_embed.set_footer(text="Tortoise community") await member.send(embed=dm_embed)
async def _suggestion_helper(self, ctx, message_id: int, reason: str, status: constants.SuggestionStatus): """ Helper for suggestion approve/deny. :param ctx: context where approve/deny command was called. :param message_id: suggestion message id :param reason: reason for approving/denying :param status: either constants.SuggestionStatus.approved or constants.SuggestionStatus.denied :return: """ msg: Message = await self.user_suggestions_channel.fetch_message( message_id) if msg is None: return await ctx.send( embed=failure("Suggestion message not found."), delete_after=10) elif not msg.embeds or not msg.embeds[0].fields: return await ctx.send( embed=failure("Message is not in correct format."), delete_after=10) api_data = await self.bot.api_client.get_suggestion(message_id) msg_embed = msg.embeds[0] if status == constants.SuggestionStatus.denied: field_title = "Reason" state = "denied" msg_embed.colour = Color.red() else: field_title = "Comment" state = "approved" msg_embed.colour = Color.green() dm_embed_msg = ( f"Your suggestion[[link]]({msg.jump_url}) was **{state}**:\n" f"```\"{api_data['brief'][:200]}\"```\n" f"\nReason:\n{reason}") dm_embed = thumbnail(dm_embed_msg, member=ctx.me, title=f"Suggestion {state}.") msg_embed.set_field_at(0, name="Status", value=status.value) if len(msg_embed.fields) == 1: msg_embed.add_field(name=field_title, value=reason, inline=True) else: msg_embed.set_field_at(1, name=field_title, value=reason, inline=True) await self.bot.api_client.edit_suggestion(message_id, status, reason) await msg.edit(embed=msg_embed) await self._dm_member(api_data["author_id"], dm_embed)
async def on_command_error(self, ctx, error_): # Get the original exception error = getattr(error_, "original", error_) # If command has local error handler, ignore if hasattr(ctx.command, "on_error"): pass elif isinstance(error, commands.CommandNotFound): pass elif isinstance(error, commands.BotMissingPermissions): fmt = self._get_missing_permission(error) _message = f"I need the **{fmt}** permission(s) to run this command." await ctx.send(embed=failure(_message)) elif isinstance(error, commands.DisabledCommand): await ctx.send(embed=failure("This command has been disabled.")) elif isinstance(error, commands.CommandOnCooldown): msg = f"This command is on cooldown, please retry in {math.ceil(error.retry_after)}s." await ctx.send(embed=failure(msg)) elif isinstance(error, commands.MissingPermissions): fmt = self._get_missing_permission(error) _message = f"You need the **{fmt}** permission(s) to use this command." await ctx.send(embed=failure(_message)) elif isinstance(error, commands.UserInputError): await ctx.send(embed=failure(f"Invalid command input: {error}")) elif isinstance(error, commands.NoPrivateMessage): try: await ctx.author.send(embed=failure("This command cannot be used in direct messages.")) except discord.Forbidden: pass elif isinstance(error, commands.CheckFailure): """-.- All arguments including error message are eaten and pushed to .args""" if error.args: await ctx.send(embed=failure(". ".join(error.args))) else: await ctx.send(embed=failure("You do not have permission to use this command.")) elif isinstance(error, discord.Forbidden): # Conditional to check if it is a closed DM that raised Forbidden if error.code == 50007: pass else: await ctx.send(embed=failure(f"{error}")) else: error_type = type(error) feedback_message = f"Uncaught {error_type} exception in command '{ctx.command}'" traceback_message = traceback.format_exception(etype=error_type, value=error, tb=error.__traceback__) log_message = f"{feedback_message} {traceback_message}" logger.critical(log_message) await self.bot.log_error(log_message)
async def submit(self, ctx): """Initializes process of submitting code for event.""" dm_msg = ( "Submitting process has begun.\n\n" "Please reply with 1 message below that either contains your full code or, " "if it's too long, contains a link to code (pastebin/hastebin..)\n" "If using those services make sure to set code to private and " "expiration date to at least 30 days." ) await ctx.author.send(embed=authored(dm_msg, author=ctx.guild.me)) def check(msg): return msg.author == ctx.author and msg.guild is None try: code_msg = await self.bot.wait_for("message", check=check, timeout=300) except TimeoutError: await ctx.send(embed=failure("You took too long to reply.")) return title = f"Submission from {ctx.author}" embed = discord.Embed(title=title, description=code_msg.content, color=ctx.me.top_role.color) embed.set_thumbnail(url=ctx.author.avatar_url) await self.code_submissions_channel.send(embed=embed)
async def on_raw_reaction_add_helper(self, payload): user_id = payload.user_id user = self.bot.get_user(user_id) if user is None: # Users cannot send messages if they do not share at least one guild with the bot, # however they can react to messages they previously sent to bot making it possible # that user will be None as they do not share a guild! return if user_id == self.bot.user.id: return # Ignore the bot elif self.is_any_session_active(user_id): return if self.cool_down.is_on_cool_down(user_id): msg = f"You are on cooldown. You can retry after {self.cool_down.retry_after(user_id)}s" await user.send(embed=failure(msg)) return else: self.cool_down.add_to_cool_down(user_id) for emoji_id, sub_dict in self._options.items(): emoji = self.bot.get_emoji(emoji_id) if sub_dict["check"]() and emoji == payload.emoji: await sub_dict["callable"](user) break
async def _wait_for(self, container: set, user: discord.User) -> Union[discord.Message, None]: """ Simple custom wait_for that waits for user reply for 5 minutes and has ability to cancel the wait, deal with errors and deal with containers (which mark users that are currently doing something aka event submission/bug report etc). :param container: set, container holding active user sessions by having their IDs in it. :param user: Discord user to wait reply from :return: Union[Message, None] message representing user reply, can be none representing invalid reply. """ def check(msg): return msg.guild is None and msg.author == user container.add(user.id) await user.send(embed=info( "Reply with single message, link to paste service or uploading utf-8 `.txt` file.\n" "You have 5m, type `cancel` to cancel right away.", user)) try: user_reply = await self.bot.wait_for("message", check=check, timeout=300) except TimeoutError: await user.send(embed=failure("You took too long to reply.")) container.remove(user.id) return if user_reply.content.lower() == "cancel": await user.send(embed=success("Successfully canceled.")) container.remove(user.id) return return user_reply
async def warn(self, ctx, member: discord.Member, *, reason): """ Warns a member. Reason length is maximum of 200 characters. """ if len(reason) > 200: await ctx.send( embed=failure("Please shorten the reason to 200 characters."), delete_after=3) return deterrence_log_channel = self.bot.get_channel( constants.deterrence_log_channel_id) embed = infraction_embed(ctx, member, constants.Infraction.warning, reason) embed.add_field( name="**NOTE**", value=("If you are planning to repeat this again, " "the mods may administer punishment for the action.")) await deterrence_log_channel.send(f"{member.mention}", delete_after=0.5) await deterrence_log_channel.send(embed=embed) await self.bot.api_client.add_member_warning(ctx.author.id, member.id, reason) await ctx.send(embed=success("Warning successfully applied.", ctx.me), delete_after=5) await asyncio.sleep(5) await ctx.message.delete()
async def show_data(self, ctx, member: DatabaseMember): try: data = await self.bot.api_client.get_member_data(member) except ResponseCodeError as e: await ctx.send(embed=failure(f"Something went wrong, got response status {e.status}.\n" f"Does the member exist?")) return await ctx.send(f"{data}")
async def is_verified(self, ctx, member: DatabaseMember): try: response = await self.bot.api_client.is_verified(member) except ResponseCodeError as e: msg = f"Something went wrong, got response status {e.status}.\nDoes the member exist?" await ctx.send(embed=failure(msg)) else: await ctx.send( embed=info(f"Verified: {response}", ctx.me, title=f"{member}"))
async def show_data(self, ctx, member: DatabaseMember): try: data = await self.bot.api_client.get_member_data(member) except ResponseCodeError as e: msg = f"Something went wrong, got response status {e.status}.\nDoes the member exist?" await ctx.send(embed=failure(msg)) else: pretty = "\n".join(f"{key}:{value}\n" for key, value in data.items()) await ctx.send(embed=info(pretty, ctx.me, "Member data"))
async def mute(self, ctx, member: discord.Member, *, reason="No reason stated."): """Mutes the member.""" if self.muted_role in member.roles: await ctx.send(embed=failure("Cannot mute as member is already muted.")) return reason = f"Muting member. {reason}" await member.add_roles(self.muted_role, reason=reason) await member.remove_roles(self.verified_role, reason=reason) await ctx.send(embed=success(f"{member} successfully muted."), delete_after=5) await self.bot.api_client.add_member_warning(ctx.author.id, member.id, reason)
async def unmute(self, ctx, member: discord.Member): """Unmutes the member.""" if self.muted_role not in member.roles: await ctx.send(embed=failure("Cannot unmute as member is not muted.")) return reason = f"Unmuted by {ctx.author.id}" await member.remove_roles(self.muted_role, reason=reason) await member.add_roles(self.verified_role, reason=reason) await ctx.send(embed=success(f"{member} successfully unmuted."), delete_after=5)
async def reload(self, ctx, extension_name): """ Reloads an extension. :param extension_name: cog name without suffix """ if extension_name == Path(__file__).stem: await ctx.send(embed=failure( "This cog is protected, cannot execute operation.")) return self.bot.reload_extension(f"bot.cogs.{extension_name}") await ctx.send(embed=success(f"{extension_name} reloaded.", ctx.me))
async def create_mod_mail(self, user: discord.User): if user.id in self.pending_mod_mails: await user.send(embed=failure("You already have a pending mod mail, please be patient.")) return submission_embed = authored(f"`{user.id}` submitted for mod mail.", author=user) # Ping roles so they get notified sooner await self.mod_mail_report_channel.send("@here", delete_after=30) await self.mod_mail_report_channel.send(embed=submission_embed) self.pending_mod_mails.add(user.id) await user.send(embed=success("Mod mail was sent to admins, please wait for one of the admins to accept."))
async def rule(self, ctx, alias: Union[int, str]): """ Shows rule based on number order or alias. """ if isinstance(alias, int): rule_dict = self._get_rule_by_value(alias) else: rule_dict = self._get_rule_by_alias(alias) if rule_dict is None: await ctx.send(embed=failure("No such rule."), delete_after=5) else: await ctx.send(embed=info(rule_dict["statement"], ctx.guild.me, f"Rule {alias}"))
async def unload(self, ctx, extension_name): """ Unloads an extension. :param extension_name: cog name without suffix """ if extension_name == Path(__file__).stem: await ctx.send(embed=failure("This cog is protected, cannot unload.")) return self.bot.unload_extension(f"bot.cogs.{extension_name}") msg = f"{extension_name} unloaded." logger.info(f"{msg} by {ctx.author.id}") await ctx.send(embed=success(f"{extension_name} unloaded.", ctx.me))
async def create_mod_mail(self, user: discord.User): if user.id in self.pending_mod_mails: await user.send(embed=failure( "You already have a pending mod mail, please be patient.")) return mod_mail_report_channel = self.bot.get_channel( constants.mod_mail_report_channel_id) submission_embed = authored(f"`{user.id}` submitted for mod mail.", author=user) await mod_mail_report_channel.send(embed=submission_embed) self.pending_mod_mails.add(user.id) await user.send(embed=success( "Mod mail was sent to admins, please wait for one of the admins to accept." ))
async def mute(self, ctx, member: discord.Member, *, reason="No reason stated."): """ Mutes the member. """ if self.muted_role in member.roles: await ctx.send( embed=failure("Cannot mute as member is already muted.")) return reason = "Muting member. " + reason await member.add_roles(self.muted_role, reason=reason) await member.remove_roles(self.verified_role, reason=reason) await ctx.send(embed=success(f"{member} successfully muted."), delete_after=5)
async def fetch_doc_links(self, ctx, key, obj): page_types = { 'latest': 'https://discordpy.readthedocs.io/en/latest', 'python': 'https://docs.python.org/3', } if obj is None: await ctx.send(page_types[key]) return if not self._doc_cache: await ctx.trigger_typing() await self.build_documentation_lookup_table(page_types) obj = re.sub(r'^(?:discord\.(?:ext\.)?)?(?:commands\.)?(.+)', r'\1', obj) if key.startswith('latest'): # point the abc.Messageable types properly: q = obj.lower() for name in dir(discord.abc.Messageable): if name[0] == '_': continue if q == name: obj = f'abc.Messageable.{name}' break cache = list(self._doc_cache[key].items()) matches = Fuzzy.finder(obj, cache, key=lambda t: t[0], lazy=False)[:8] if len(matches) == 0: await ctx.send(embed=failure("Query didn't match any entity")) return embed_msg = "\n".join(f"[`{key}`]({url})" for key, url in matches) embed_msg = info(embed_msg, ctx.me, title="Links") await ctx.send(embed=embed_msg)
async def attend(self, ctx, user_id: int): # Time to wait for FIRST USER reply. Useful if mod attends but user is away. first_timeout = 10_800 # Flag for above variable. False means there has been no messages from the user. first_timeout_flag = False # After the user sends first reply this is the timeout we use. regular_timeout = 600 user = self.bot.get_user(user_id) mod = ctx.author # Keep a log of all messages in mod-mail log = MessageLogger(mod.id, user.id) mod_mail_report_channel = self.bot.get_channel( constants.mod_mail_report_channel_id) if user is None: await ctx.send(embed=failure( "That user cannot be found or you entered incorrect ID.")) return elif user_id not in self.pending_mod_mails: await ctx.send( embed=failure("That user is not registered for mod mail.")) return elif self.is_any_session_active(mod.id): await ctx.send(embed=failure( "You already have one of active sessions (reports/mod mail etc)." )) return self.pending_mod_mails.remove(user_id) self.active_mod_mails[user_id] = mod.id await user.send(embed=authored(( "has accepted your mod mail request.\n" "Reply here in DMs to chat with them.\n" "This mod mail will be logged, by continuing you agree to that.\n" "Type `close` to close this mod mail."), author=mod)) await mod.send( embed=success(f"You have accepted `{user}` mod mail request.\n" "Reply here in DMs to chat with them.\n" "This mod mail will be logged.\n" "Type `close` to close this mod mail.")) await ctx.send(embed=success("Mod mail initialized, check your DMs."), delete_after=10) def mod_mail_check(msg): return msg.guild is None and msg.author.id in (user_id, mod.id) _timeout = first_timeout while True: try: mail_msg = await self.bot.wait_for("message", check=mod_mail_check, timeout=_timeout) log.add_message(mail_msg) except TimeoutError: timeout_embed = failure("Mod mail closed due to inactivity.") log.add_embed(timeout_embed) await mod.send(embed=timeout_embed) await user.send(embed=timeout_embed) del self.active_mod_mails[user_id] await mod_mail_report_channel.send(file=discord.File( StringIO(str(log)), filename=log.filename)) break # Deal with dynamic timeout. if mail_msg.author == user and not first_timeout_flag: first_timeout_flag = True _timeout = regular_timeout # Deal with canceling mod mail if mail_msg.content.lower() == "close": close_embed = success( f"Mod mail successfully closed by {mail_msg.author}.") log.add_embed(close_embed) await mod.send(embed=close_embed) await user.send(embed=close_embed) del self.active_mod_mails[user_id] await mod_mail_report_channel.send(file=discord.File( StringIO(str(log)), filename=log.filename)) break # Deal with user-mod communication if mail_msg.author == user: await mod.send(mail_msg.content) elif mail_msg.author == mod: await user.send(mail_msg.content)
async def attend(self, ctx, user_id: int): if not any(role in ctx.author.roles for role in (self.admin_role, self.moderator_role)): await ctx.send(embed=failure("You do not have permission to use this command.")) return # Time to wait for FIRST USER reply. Useful if mod attends but user is away. first_timeout = 21_600 # 6 hours # Flag for above variable. False means there has been no messages from the user. first_timeout_flag = False # After the user sends first reply this is the timeout we use. regular_timeout = 1800 # 30 min user = self.bot.get_user(user_id) mod = ctx.author if user is None: await ctx.send(embed=failure("That user cannot be found or you entered incorrect ID.")) return elif user_id not in self.pending_mod_mails: await ctx.send(embed=failure("That user is not registered for mod mail.")) return elif self.is_any_session_active(mod.id): await ctx.send(embed=failure("You already have one of active sessions (reports/mod mail etc).")) return try: await mod.send( embed=success( f"You have accepted `{user}` mod mail request.\n" "Reply here in DMs to chat with them.\n" "This mod mail will be logged.\n" "Type `close` to close this mod mail." ) ) except discord.HTTPException: await ctx.send(embed=failure("Mod mail failed to initialize due to mod having closed DMs.")) return # Unlike failing for mods due to closed DMs this cannot fail for user since user already did interact # with bot in DMs as he needs to in order to even open mod-mail. await user.send( embed=authored( ( "has accepted your mod mail request.\n" "Reply here in DMs to chat with them.\n" "This mod mail will be logged, by continuing you agree to that." ), author=mod ) ) await ctx.send(embed=success("Mod mail initialized, check your DMs.")) self.pending_mod_mails.remove(user_id) self.active_mod_mails[user_id] = mod.id _timeout = first_timeout # Keep a log of all messages in mod-mail log = MessageLogger(mod.id, user.id) def mod_mail_check(msg): return msg.guild is None and msg.author.id in (user_id, mod.id) while True: try: mail_msg = await self.bot.wait_for("message", check=mod_mail_check, timeout=_timeout) log.add_message(mail_msg) except TimeoutError: timeout_embed = failure("Mod mail closed due to inactivity.") log.add_embed(timeout_embed) await mod.send(embed=timeout_embed) await user.send(embed=timeout_embed) del self.active_mod_mails[user_id] await self.mod_mail_report_channel.send(file=discord.File(StringIO(str(log)), filename=log.filename)) break # Deal with attachments. We don't re-upload we just copy paste attachment url. attachments = self._get_attachments_as_urls(mail_msg) mail_msg.content += attachments if len(mail_msg.content) > 1900: mail_msg.content = f"{mail_msg.content[:1900]} ...truncated because it was too long." # Deal with dynamic timeout. if mail_msg.author == user and not first_timeout_flag: first_timeout_flag = True _timeout = regular_timeout # Deal with canceling mod mail if mail_msg.content.lower() == "close" and mail_msg.author.id == mod.id: close_embed = success(f"Mod mail successfully closed by {mail_msg.author}.") log.add_embed(close_embed) await mod.send(embed=close_embed) await user.send(embed=close_embed) del self.active_mod_mails[user_id] await self.mod_mail_report_channel.send(file=discord.File(StringIO(str(log)), filename=log.filename)) break # Deal with user-mod communication if mail_msg.author == user: await mod.send(mail_msg.content) elif mail_msg.author == mod: await user.send(mail_msg.content)