Exemplo n.º 1
0
    async def move(self, ctx, category: discord.CategoryChannel, *, specifics: str = None):
        """
        Move a thread to another category.

        `category` may be a category ID, mention, or name.
        `specifics` is a string which takes in arguments on how to perform the move. Ex: "silently"
        """
        thread = ctx.thread
        silent = False

        if specifics:
            silent_words = ['silent', 'silently']
            silent = any(word in silent_words for word in specifics.split())

        await thread.channel.edit(category=category, sync_permissions=True)

        try:
            thread_move_notify = strtobool(self.bot.config["thread_move_notify"])
        except ValueError:
            thread_move_notify = self.bot.config.remove("thread_move_notify")

        if thread_move_notify and not silent:
            embed = discord.Embed(
                title="Thread Moved",
                description=self.bot.config["thread_move_response"],
                color=self.bot.main_color
            )
            await thread.recipient.send(embed=embed)

        sent_emoji, _ = await self.bot.retrieve_emoji()
        try:
            await ctx.message.add_reaction(sent_emoji)
        except (discord.HTTPException, discord.InvalidArgument):
            pass
Exemplo n.º 2
0
    def set(self, key: str, item: typing.Any, convert=True) -> None:
        if not convert:
            return self.__setitem__(key, item)

        if key in self.colors:
            try:
                hex_ = str(item)
                if hex_.startswith("#"):
                    hex_ = hex_[1:]
                if len(hex_) == 3:
                    hex_ = "".join(s for s in hex_ for _ in range(2))
                if len(hex_) != 6:
                    raise InvalidConfigError("Invalid color name or hex.")
                try:
                    int(hex_, 16)
                except ValueError:
                    raise InvalidConfigError("Invalid color name or hex.")

            except InvalidConfigError:
                name = str(item).lower()
                name = re.sub(r"[\-+|. ]+", " ", name)
                hex_ = ALL_COLORS.get(name)
                if hex_ is None:
                    name = re.sub(r"[\-+|. ]+", "", name)
                    hex_ = ALL_COLORS.get(name)
                    if hex_ is None:
                        raise
            return self.__setitem__(key, "#" + hex_)

        if key in self.time_deltas:
            try:
                isodate.parse_duration(item)
            except isodate.ISO8601Error:
                try:
                    converter = UserFriendlyTime()
                    time = self.bot.loop.run_until_complete(
                        converter.convert(None, item)
                    )
                    if time.arg:
                        raise ValueError
                except BadArgument as exc:
                    raise InvalidConfigError(*exc.args)
                except Exception:
                    raise InvalidConfigError(
                        "Unrecognized time, please use ISO-8601 duration format "
                        'string or a simpler "human readable" time.'
                    )
                item = isodate.duration_isoformat(time.dt - converter.now)
            return self.__setitem__(key, item)

        if key in self.booleans:
            try:
                return self.__setitem__(key, strtobool(item))
            except ValueError:
                raise InvalidConfigError("Must be a yes/no value.")

        # elif key in self.special_types:
        #     if key == "status":

        return self.__setitem__(key, item)
Exemplo n.º 3
0
    def set(self, key: str, item: typing.Any, convert=True) -> None:
        if not convert:
            return self.__setitem__(key, item)

        if key in self.colors:
            try:
                hex_ = str(item)
                if hex_.startswith("#"):
                    hex_ = hex_[1:]
                if len(hex_) == 3:
                    hex_ = "".join(s for s in hex_ for _ in range(2))
                if len(hex_) != 6:
                    raise InvalidConfigError(
                        "Nombre de color o HEX inválido.")
                try:
                    int(hex_, 16)
                except ValueError:
                    raise InvalidConfigError(
                        "Nombre de color o HEX inválido.")

            except InvalidConfigError:
                name = str(item).lower()
                name = re.sub(r"[\-+|. ]+", " ", name)
                hex_ = ALL_COLORS.get(name)
                if hex_ is None:
                    name = re.sub(r"[\-+|. ]+", "", name)
                    hex_ = ALL_COLORS.get(name)
                    if hex_ is None:
                        raise
            return self.__setitem__(key, "#" + hex_)

        if key in self.time_deltas:
            try:
                isodate.parse_duration(item)
            except isodate.ISO8601Error:
                try:
                    converter = UserFriendlyTimeSync()
                    time = converter.convert(None, item)
                    if time.arg:
                        raise ValueError
                except BadArgument as exc:
                    raise InvalidConfigError(*exc.args)
                except Exception as e:
                    logger.debug(e)
                    raise InvalidConfigError(
                        "Tiempo no reconocido, por favor use el formato de duración ISO-8601 "
                        'o un tiempo de "lectura humana" más simple.')
                item = isodate.duration_isoformat(time.dt - converter.now)
            return self.__setitem__(key, item)

        if key in self.booleans:
            try:
                return self.__setitem__(key, strtobool(item))
            except ValueError:
                raise InvalidConfigError("Debe ser un valor de Sí/No.")

        # elif key in self.special_types:
        #     if key == "status":

        return self.__setitem__(key, item)
Exemplo n.º 4
0
    def get(self, key: str, convert=True) -> typing.Any:
        value = self.__getitem__(key)

        if not convert:
            return value

        if key in self.colors:
            try:
                return int(value.lstrip("#"), base=16)
            except ValueError:
                logger.error("Invalid %s provided.", key)
            value = int(self.remove(key).lstrip("#"), base=16)

        elif key in self.time_deltas:
            if value is None:
                return
            try:
                value = isodate.parse_duration(value)
            except isodate.ISO8601Error:
                logger.warning(
                    "The {account} age limit needs to be a "
                    'ISO-8601 duration formatted duration, not "%s".',
                    value,
                )
                value = self.remove(key)

        elif key in self.booleans:
            try:
                value = strtobool(value)
            except ValueError:
                value = self.remove(key)

        elif key in self.special_types:
            if value is None:
                return

            if key == "status":
                try:
                    # noinspection PyArgumentList
                    value = discord.Status(value)
                except ValueError:
                    logger.warning("Invalid status %s.", value)
                    value = self.remove(key)

            elif key == "activity_type":
                try:
                    # noinspection PyArgumentList
                    value = discord.ActivityType(value)
                except ValueError:
                    logger.warning("Invalid activity %s.", value)
                    value = self.remove(key)

        return value
Exemplo n.º 5
0
    async def clean_data(self, key: str,
                         val: typing.Any) -> typing.Tuple[str, str]:
        value_text = val
        clean_value = val

        # when setting a color
        if key in self.colors:
            hex_ = ALL_COLORS.get(val)

            if hex_ is None:
                hex_ = str(hex_)
                if hex_.startswith("#"):
                    hex_ = hex_[1:]
                if len(hex_) == 3:
                    hex_ = "".join(s for s in hex_ for _ in range(2))
                if len(hex_) != 6:
                    raise InvalidConfigError("Invalid color name or hex.")
                try:
                    int(val, 16)
                except ValueError:
                    raise InvalidConfigError("Invalid color name or hex.")
                clean_value = "#" + val
                value_text = clean_value
            else:
                clean_value = hex_
                value_text = f"{val} ({clean_value})"

        elif key in self.time_deltas:
            try:
                isodate.parse_duration(val)
            except isodate.ISO8601Error:
                try:
                    converter = UserFriendlyTime()
                    time = await converter.convert(None, val)
                    if time.arg:
                        raise ValueError
                except BadArgument as exc:
                    raise InvalidConfigError(*exc.args)
                except Exception:
                    raise InvalidConfigError(
                        "Unrecognized time, please use ISO-8601 duration format "
                        'string or a simpler "human readable" time.')
                clean_value = isodate.duration_isoformat(time.dt -
                                                         converter.now)
                value_text = f"{val} ({clean_value})"

        elif key in self.booleans:
            try:
                clean_value = value_text = strtobool(val)
            except ValueError:
                raise InvalidConfigError("Must be a yes/no value.")

        return clean_value, value_text
Exemplo n.º 6
0
    async def _restart_close_timer(self):
        """
        This will create or restart a timer to automatically close this
        thread.
        """
        timeout = await self._fetch_timeout()

        # Exit if timeout was not set
        if not timeout:
            return

        # Set timeout seconds
        seconds = timeout.total_seconds()
        # seconds = 20  # Uncomment to debug with just 20 seconds
        reset_time = datetime.utcnow() + timedelta(seconds=seconds)
        human_time = human_timedelta(dt=reset_time)

        try:
            thread_auto_close_silently = strtobool(
                self.bot.config["thread_auto_close_silently"]
            )
        except ValueError:
            thread_auto_close_silently = self.bot.config.remove(
                "thread_auto_close_silently"
            )

        if thread_auto_close_silently:
            return await self.close(
                closer=self.bot.user, silent=True, after=int(seconds), auto_close=True
            )

        # Grab message
        close_message = self.bot.formatter.format(
            self.bot.config["thread_auto_close_response"],
            timeout=human_time
        )

        time_marker_regex = "%t"
        if len(re.findall(time_marker_regex, close_message)) == 1:
            close_message = re.sub(time_marker_regex, str(human_time), close_message)
        elif len(re.findall(time_marker_regex, close_message)) > 1:
            logger.warning(
                "The thread_auto_close_response should only contain one '%s' to specify time.",
                time_marker_regex,
            )

        await self.close(
            closer=self.bot.user,
            after=int(seconds),
            message=close_message,
            auto_close=True,
        )
Exemplo n.º 7
0
    def get(self, key: str, convert=True) -> typing.Any:
        value = self.__getitem__(key)

        if not convert:
            return value

        if key in self.colors:
            try:
                return int(value.lstrip("#"), base=16)
            except ValueError:
                logger.error("Inválido %s.", key)
            value = int(self.remove(key).lstrip("#"), base=16)

        elif key in self.time_deltas:
            if not isinstance(value, isodate.Duration):
                try:
                    value = isodate.parse_duration(value)
                except isodate.ISO8601Error:
                    logger.warning(
                        "El límite de edad de la cuenta ${account} debe ser un"
                        'ISO-8601 duración formateada, no "%s".',
                        value,
                    )
                    value = self.remove(key)

        elif key in self.booleans:
            try:
                value = strtobool(value)
            except ValueError:
                value = self.remove(key)

        elif key in self.special_types:
            if value is None:
                return None

            if key == "status":
                try:
                    # noinspection PyArgumentList
                    value = discord.Status(value)
                except ValueError:
                    logger.warning("Estado inválido %s.", value)
                    value = self.remove(key)

            elif key == "activity_type":
                try:
                    # noinspection PyArgumentList
                    value = discord.ActivityType(value)
                except ValueError:
                    logger.warning("Actividad inválida %s.", value)
                    value = self.remove(key)

        return value
Exemplo n.º 8
0
    def get(self, key: str, convert=True) -> typing.Any:
        value = self.__getitem__(key)

        if not convert:
            return value

        if key in self.colors:
            try:
                return int(value.lstrip("#"), base=16)
            except ValueError:
                logger.error("Invalid %s provided.", key)
            value = int(self.remove(key).lstrip("#"), base=16)

        elif key in self.time_deltas:
            if not isinstance(value, isodate.Duration):
                try:
                    value = isodate.parse_duration(value)
                except isodate.ISO8601Error:
                    logger.warning(
                        "L'età di {account} deve essere una durata "
                        'formattata in ISO-8601, non "%s".',
                        value,
                    )
                    value = self.remove(key)

        elif key in self.booleans:
            try:
                value = strtobool(value)
            except ValueError:
                value = self.remove(key)

        elif key in self.special_types:
            if value is None:
                return None

            if key == "status":
                try:
                    # noinspection PyArgumentList
                    value = discord.Status(value)
                except ValueError:
                    logger.warning("Stato non valido: %s.", value)
                    value = self.remove(key)

            elif key == "activity_type":
                try:
                    # noinspection PyArgumentList
                    value = discord.ActivityType(value)
                except ValueError:
                    logger.warning("Attività non valida: %s.", value)
                    value = self.remove(key)

        return value
Exemplo n.º 9
0
    async def on_typing(self, channel, user, _):
        await self.wait_for_connected()

        if user.bot:
            return

        async def _void(*_args, **_kwargs):
            pass

        if isinstance(channel, discord.DMChannel):
            try:
                user_typing = strtobool(self.config["user_typing"])
            except ValueError:
                user_typing = self.config.remove("user_typing")
            if not user_typing:
                return

            thread = await self.threads.find(recipient=user)

            if thread:
                await thread.channel.trigger_typing()
        else:
            try:
                mod_typing = strtobool(self.config["mod_typing"])
            except ValueError:
                mod_typing = self.config.remove("mod_typing")
            if not mod_typing:
                return

            thread = await self.threads.find(channel=channel)
            if thread is not None and thread.recipient:
                if await self._process_blocked(
                        SimpleNamespace(
                            author=thread.recipient,
                            channel=SimpleNamespace(send=_void),
                            add_reaction=_void,
                        )):
                    return
                await thread.recipient.trigger_typing()
Exemplo n.º 10
0
    async def process_commands(self, message):
        if message.author.bot:
            return

        if isinstance(message.channel, discord.DMChannel):
            return await self.process_dm_modmail(message)

        if message.content.startswith(self.prefix):
            cmd = message.content[len(self.prefix):].strip()

            # Process snippets
            if cmd in self.snippets:
                thread = await self.threads.find(channel=message.channel)
                snippet = self.snippets[cmd]
                if thread:
                    snippet = self.formatter.format(snippet,
                                                    recipient=thread.recipient)
                message.content = f"{self.prefix}reply {snippet}"

        ctxs = await self.get_contexts(message)
        for ctx in ctxs:
            if ctx.command:
                if not any(1 for check in ctx.command.checks
                           if hasattr(check, "permission_level")):
                    logger.debug(
                        "Command %s has no permissions check, adding invalid level.",
                        ctx.command.qualified_name,
                    )
                    checks.has_permissions(PermissionLevel.INVALID)(
                        ctx.command)

                await self.invoke(ctx)
                continue

            thread = await self.threads.find(channel=ctx.channel)
            if thread is not None:
                try:
                    reply_without_command = strtobool(
                        self.config["reply_without_command"])
                except ValueError:
                    reply_without_command = self.config.remove(
                        "reply_without_command")

                if reply_without_command:
                    await thread.reply(message)
                else:
                    await self.api.append_log(message, type_="internal")
            elif ctx.invoked_with:
                exc = commands.CommandNotFound(
                    'Command "{}" is not found'.format(ctx.invoked_with))
                self.dispatch("command_error", ctx, exc)
Exemplo n.º 11
0
    async def on_raw_reaction_add(self, payload):
        user = self.get_user(payload.user_id)
        if user.bot:
            return

        channel = self.get_channel(payload.channel_id)
        if not channel:  # dm channel not in internal cache
            _thread = await self.threads.find(recipient=user)
            if not _thread:
                return
            channel = await _thread.recipient.create_dm()

        try:
            message = await channel.fetch_message(payload.message_id)
        except discord.NotFound:
            return

        reaction = payload.emoji

        close_emoji = await self.convert_emoji(self.config["close_emoji"])

        if isinstance(channel, discord.DMChannel):
            if str(reaction) == str(close_emoji):  # closing thread
                try:
                    recipient_thread_close = strtobool(
                        self.config["recipient_thread_close"])
                except ValueError:
                    recipient_thread_close = self.config.remove(
                        "recipient_thread_close")
                if not recipient_thread_close:
                    return
                thread = await self.threads.find(recipient=user)
                ts = message.embeds[0].timestamp if message.embeds else None
                if thread and ts == thread.channel.created_at:
                    # the reacted message is the corresponding thread creation embed
                    await thread.close(closer=user)
        else:
            if not message.embeds:
                return
            message_id = str(message.embeds[0].author.url).split("/")[-1]
            if message_id.isdigit():
                thread = await self.threads.find(channel=message.channel)
                channel = thread.recipient.dm_channel
                if not channel:
                    channel = await thread.recipient.create_dm()
                async for msg in channel.history():
                    if msg.id == int(message_id):
                        await msg.add_reaction(reaction)
Exemplo n.º 12
0
    def get(self, key: str, convert=True) -> typing.Any:
        value = self.__getitem__(key)

        if not convert:
            return value

        if key in self.colors:
            try:
                return int(value.lstrip("#"), base=16)
            except ValueError:
                logger.error("Invalid %s provided.", key)
            value = int(self.remove(key).lstrip("#"), base=16)

        elif key in self.time_deltas:
            if not isinstance(value, isodate.Duration):
                try:
                    value = isodate.parse_duration(value)
                except isodate.ISO8601Error:
                    logger.warning(
                        "The {account} age limit needs to be a "
                        'ISO-8601 duration formatted duration, not "%s".',
                        value,
                    )
                    value = self.remove(key)

        elif key in self.booleans:
            try:
                value = strtobool(value)
            except ValueError:
                value = self.remove(key)

        elif key in self.enums:
            if value is None:
                return None
            try:
                value = self.enums[key](value)
            except ValueError:
                logger.warning("Invalid %s %s.", key, value)
                value = self.remove(key)

        return value
Exemplo n.º 13
0
    async def process_commands(self, message):
        if message.author.bot:
            return

        if isinstance(message.channel, discord.DMChannel):
            return await self.process_dm_modmail(message)

        if message.content.startswith(self.prefix):
            cmd = message.content[len(self.prefix):].strip()

            # Process snippets
            if cmd in self.snippets:
                thread = await self.threads.find(channel=message.channel)
                snippet = self.snippets[cmd]
                if thread:
                    snippet = snippet.format(recipient=thread.recipient)
                message.content = f"{self.prefix}reply {snippet}"

        ctxs = await self.get_contexts(message)
        for ctx in ctxs:
            if ctx.command:
                await self.invoke(ctx)
                continue

            thread = await self.threads.find(channel=ctx.channel)
            if thread is not None:
                try:
                    reply_without_command = strtobool(
                        self.config["reply_without_command"])
                except ValueError:
                    reply_without_command = self.config.remove(
                        "reply_without_command")

                if reply_without_command:
                    await thread.reply(message)
                else:
                    await self.api.append_log(message, type_="internal")
            elif ctx.invoked_with:
                exc = commands.CommandNotFound(
                    'Command "{}" is not found'.format(ctx.invoked_with))
                self.dispatch("command_error", ctx, exc)
Exemplo n.º 14
0
    def get(self, key: str, convert=True) -> typing.Any:
        key = key.lower()
        if key not in self.all_keys:
            raise InvalidConfigError(f'Configuration "{key}" is invalid.')
        if key not in self._cache:
            self._cache[key] = deepcopy(self.defaults[key])
        value = self._cache[key]

        if not convert:
            return value

        if key in self.colors:
            try:
                return int(value.lstrip("#"), base=16)
            except ValueError:
                logger.error("Invalid %s provided.", key)
            value = int(self.remove(key).lstrip("#"), base=16)

        elif key in self.time_deltas:
            if not isinstance(value, isodate.Duration):
                try:
                    value = isodate.parse_duration(value)
                except isodate.ISO8601Error:
                    logger.warning(
                        "The {account} age limit needs to be a "
                        'ISO-8601 duration formatted duration, not "%s".',
                        value,
                    )
                    value = self.remove(key)

        elif key in self.booleans:
            try:
                value = strtobool(value)
            except ValueError:
                value = self.remove(key)

        elif key in self.enums:
            if value is None:
                return None
            try:
                value = self.enums[key](value)
            except ValueError:
                logger.warning("Invalid %s %s.", key, value)
                value = self.remove(key)

        elif key in self.force_str:
            # Temporary: as we saved in int previously, leading to int32 overflow,
            #            this is transitioning IDs to strings
            new_value = {}
            changed = False
            for k, v in value.items():
                new_v = v
                if isinstance(v, list):
                    new_v = []
                    for n in v:
                        if n != -1 and not isinstance(n, str):
                            changed = True
                            n = str(n)
                        new_v.append(n)
                new_value[k] = new_v

            if changed:
                # transition the database as well
                self.set(key, new_value)

            value = new_value

        return value
Exemplo n.º 15
0
    async def setup(self, *, creator=None, category=None):
        """Create the thread channel and other io related initialisation tasks"""

        self.bot.dispatch("thread_create", self)

        recipient = self.recipient

        # in case it creates a channel outside of category
        overwrites = {
            self.bot.modmail_guild.default_role: discord.PermissionOverwrite(
                read_messages=False
            )
        }

        category = category or self.bot.main_category

        if category is not None:
            overwrites = None

        try:
            channel = await self.bot.modmail_guild.create_text_channel(
                name=self.manager.format_channel_name(recipient),
                category=category,
                overwrites=overwrites,
                reason="Creating a thread channel.",
            )
        except discord.HTTPException as e:  # Failed to create due to 50 channel limit.
            logger.critical("An error occurred while creating a thread.", exc_info=True)
            self.manager.cache.pop(self.id)

            embed = discord.Embed(color=discord.Color.red())
            embed.title = "Error while trying to create a thread."
            embed.description = str(e)
            embed.add_field(name="Recipient", value=recipient.mention)

            if self.bot.log_channel is not None:
                await self.bot.log_channel.send(embed=embed)
            return

        self._channel = channel

        try:
            log_url, log_data = await asyncio.gather(
                self.bot.api.create_log_entry(recipient, channel, creator or recipient),
                self.bot.api.get_user_logs(recipient.id),
            )

            log_count = sum(1 for log in log_data if not log["open"])
        except Exception:
            logger.error(
                "An error occurred while posting logs to the database.", exc_info=True
            )
            log_url = log_count = None
            # ensure core functionality still works

        if creator:
            mention = None
        else:
            mention = self.bot.config["mention"]

        async def send_genesis_message():
            info_embed = self._format_info_embed(
                recipient, log_url, log_count, discord.Color.green()
            )
            try:
                msg = await channel.send(mention, embed=info_embed)
                self.bot.loop.create_task(msg.pin())
                self.genesis_message = msg
            except Exception:
                logger.error("Failed unexpectedly:", exc_info=True)
            finally:
                self.ready = True

        await channel.edit(topic=f"User ID: {recipient.id}")
        self.bot.loop.create_task(send_genesis_message())

        # Once thread is ready, tell the recipient.
        thread_creation_response = self.bot.config["thread_creation_response"]

        embed = discord.Embed(
            color=self.bot.mod_color,
            description=thread_creation_response,
            timestamp=channel.created_at,
        )

        try:
            recipient_thread_close = strtobool(
                self.bot.config["recipient_thread_close"]
            )
        except ValueError:
            recipient_thread_close = self.bot.config.remove("recipient_thread_close")

        if recipient_thread_close:
            footer = self.bot.config["thread_self_closable_creation_footer"]
        else:
            footer = self.bot.config["thread_creation_footer"]

        embed.set_footer(text=footer, icon_url=self.bot.guild.icon_url)
        embed.title = self.bot.config["thread_creation_title"]

        if creator is None:
            msg = await recipient.send(embed=embed)

            if recipient_thread_close:
                close_emoji = self.bot.config["close_emoji"]
                close_emoji = await self.bot.convert_emoji(close_emoji)
                await msg.add_reaction(close_emoji)