Esempio n. 1
0
    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()
Esempio n. 2
0
    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
Esempio n. 3
0
    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)
Esempio n. 4
0
    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)
Esempio n. 5
0
    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)
Esempio n. 6
0
    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)
Esempio n. 7
0
    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
Esempio n. 8
0
    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
Esempio n. 9
0
    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()
Esempio n. 10
0
    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}")
Esempio n. 11
0
 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}"))
Esempio n. 12
0
 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"))
Esempio n. 13
0
    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)
Esempio n. 14
0
    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)
Esempio n. 15
0
    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))
Esempio n. 16
0
    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."))
Esempio n. 17
0
    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}"))
Esempio n. 18
0
    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))
Esempio n. 19
0
    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."
        ))
Esempio n. 20
0
    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)
Esempio n. 21
0
    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)
Esempio n. 22
0
    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)
Esempio n. 23
0
    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)