Ejemplo n.º 1
0
    def entrypoint(self):
        # Prepare signal handler
        signal.signal(signal.SIGTERM, self.shutdown)
        signal.signal(signal.SIGINT, self.shutdown)

        if os.environ.get('DISCORD_TOKEN'):
            LOG.info(
                "Loading API key from environment variable DISCORD_TOKEN.")
        elif self.config.get('apiKey') is None:
            if HuskyUtils.is_docker():
                LOG.critical(
                    "Please specify the API key by using the DISCORD_TOKEN environment varaible when using "
                    "Docker.")
                exit(1)

            if self.__daemon_mode:
                LOG.critical(
                    "The bot does not have an API key assigned to it. Please either specify a key in the env "
                    "variable DISCORD_TOKEN, add a key to the config, or run this bot in non-daemon mode."
                )
                exit(1)
            else:
                print(
                    "The bot does not have an API key defined. Please enter one below..."
                )
                key = input("Discord API Key? ")

                self.config.set('apiKey', key)
                print("The API key has been set!")

        LOG.info("The bot's log path is: {}".format(self.__log_path))

        if HuskyUtils.is_docker():
            LOG.info(
                "The bot has detected it is running in Docker. Some internal systems have been changed in order "
                "to better suit the container land.")

        if self.__daemon_mode:
            LOG.info(
                "The bot is currently loaded in Daemon Mode. In Daemon Mode, certain functionalities are "
                "slightly altered to better utilize the headless environment.")

        if self.developer_mode:
            LOG.info(
                "The bot is running in DEVELOPER MODE! Some features may behave in unexpected ways or may "
                "otherwise break. Some bot safety checks are disabled with this mode on."
            )

        self.run(os.environ.get('DISCORD_TOKEN') or self.config['apiKey'])

        if self.config.get("restartReason") is not None:
            print("READY FOR RESTART!")
            os.execl(sys.executable, *([sys.executable] + sys.argv))
Ejemplo n.º 2
0
    async def about(self, ctx: discord.ext.commands.Context):
        """
        This command returns a quick summary of this bot and its current state.
        """

        repo = git.Repo(search_parent_directories=True)
        sha = repo.head.object.hexsha

        debug_str = '| Developer Build' if self.bot.developer_mode else ''

        embed = discord.Embed(
            title=f"About {self.bot.user.name} {debug_str}",
            description="This bot (known in code as **HuskyBot**) is a custom-made Discord moderation and management "
                        "utility bot initially for [DIY Tech](https://discord.gg/diytech). It's an implementation of "
                        "the WolfBot platform for Discord, built on the popular "
                        "[discord.py rewrite](https://github.com/Rapptz/discord.py). It features seamless integration "
                        "with any workflow, and some of the most powerful plugin management and integration features "
                        "available in any commercial Discord bot. HuskyBot is built for speed and reliability for "
                        "guilds of any size, as well as easy and intuitive administration.",
            color=Colors.INFO
        )

        embed.add_field(name="Authors", value="[KazWolfe](https://github.com/KazWolfe), "
                                              "[Clover](https://github.com/cclover550)", inline=False)
        embed.add_field(name="Bot Version", value=f"[`{sha[:8]}`]({GIT_URL}/commit/{sha})", inline=True)
        embed.add_field(name="Library Version", value=f"discord.py {discord.__version__}", inline=True)
        embed.add_field(name="Python Version", value=f"Python {platform.python_version()}")
        embed.add_field(name="Current Host", value=f"`{socket.gethostname()}`", inline=True)

        platform_type = HuskyUtils.get_platform_type()
        if platform_type:
            pl_pretty_index = {"compose": "Docker (using Compose)", "systemd": "Linux (systemd)"}

            embed.add_field(name="Platform", value=pl_pretty_index.get(platform_type, platform_type), inline=True)

        init_time = self._session_store.get('initTime')
        if init_time:
            uptime = datetime.datetime.now() - init_time
            embed.add_field(
                name="Uptime",
                value=HuskyUtils.get_delta_timestr(uptime),
                inline=True
            )

        embed.set_thumbnail(url=ctx.bot.user.avatar_url)
        embed.set_footer(text=f"(c) {datetime.datetime.now().year}, KazWolfe | Andwooooooo!",
                         icon_url="https://avatars3.githubusercontent.com/u/5192145")

        await ctx.send(embed=embed)
Ejemplo n.º 3
0
    async def process_message(self, message: discord.Message, context: str):
        # config loading
        as_config: dict = self._config.get("antiSpam", {})
        global_config: dict = as_config.get("__global__", {})
        exemption_config: list = global_config.get("exemptedRoles", [])

        if not HuskyUtils.should_process_message(message):
            return

        if exemption_config and HuskyUtils.member_has_any_role(
                message.author, exemption_config):
            return

        for module in self.__modules__.values():
            asyncio.ensure_future(module.process_message(message, context))
Ejemplo n.º 4
0
    def __initialize_logger(self):
        # Build the to-file logger HuskyBot uses.
        file_log_handler = HuskyUtils.CompressingRotatingFileHandler(
            self.session_store.get('logPath'),
            maxBytes=(1024**2) * 5,
            backupCount=5,
            encoding='utf-8')
        file_log_handler.setFormatter(
            logging.Formatter(
                "%(asctime)s [%(levelname)s] %(name)s: %(message)s"))

        # Build the to-stream logger
        stream_log_handler = logging.StreamHandler(sys.stdout)
        if self.__daemon_mode:
            stream_log_handler.setFormatter(
                logging.Formatter("[%(levelname)s] %(name)s: %(message)s"))

        logging.basicConfig(
            level=logging.WARNING,
            format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
            datefmt="%Y-%m-%d %H:%M:%S",
            handlers=[file_log_handler, stream_log_handler])

        bot_logger = logging.getLogger("HuskyBot")
        bot_logger.setLevel(logging.INFO)

        if self.developer_mode:
            bot_logger.setLevel(logging.DEBUG)
            LOG.setLevel(logging.DEBUG)

        return bot_logger
Ejemplo n.º 5
0
    async def calculate_entropy(self, message: discord.Message):
        if message.content is None or message.content == "":
            return

        # run on about 20% of messages
        if random.randint(1, 5) != 3:
            return

        entropy = HuskyUtils.calculate_str_entropy(message.content)

        clean_content = message.content.replace('\n', ' // ')
        s = clean_content if len(
            clean_content) < 20 else f"{clean_content[:20]}..."

        LOG.info(
            f"[EntropyCalc] Message {message.id} in #{message.channel.name} ({s}) has "
            f"length={len(message.content)} and entropy {entropy}.")

        with open("logs/entropy.log", 'a') as f:
            f.write(
                json.dumps({
                    "text": message.content,
                    "entropy": entropy,
                    "length": len(message.content)
                }) + "\n")
Ejemplo n.º 6
0
    async def get_response(self, ctx: commands.Context, *, trigger: str):
        responses = self._config.get("responses", {})

        try:
            response = responses[trigger.lower()]
        except KeyError:
            await ctx.send(embed=discord.Embed(
                title="Response Manager",
                description=f"The trigger `{trigger}` does not exist.",
                color=Colors.DANGER))
            return

        embed = discord.Embed(color=Colors.INFO,
                              title=f"Response Data for {trigger.lower()}")

        for k in response.keys():
            v = response[k]
            if k == "response" and response.get('isEmbed', False):
                v = "< Embedded JSON >"

            embed.add_field(name=k,
                            value=HuskyUtils.trim_string(str(v), 1000),
                            inline=True)

        await ctx.send(embed=embed)
Ejemplo n.º 7
0
    async def user_unban_logger(self, guild: discord.Guild,
                                user: discord.User):
        if "userBan" not in self._config.get("loggers", {}).keys():
            return

        logger_ignores: dict = self._session_store.get('loggerIgnores', {})
        ignored_bans = logger_ignores.setdefault('ban', [])

        if user.id in ignored_bans:
            return

        alert_channel = self._config.get('specialChannels',
                                         {}).get(ChannelKeys.STAFF_LOG.value,
                                                 None)

        if alert_channel is None:
            return

        alert_channel = self.bot.get_channel(alert_channel)

        embed = discord.Embed(
            title=Emojis.UNBAN + " User unbanned",
            description=f"{user} was unbanned from the guild.",
            color=Colors.PRIMARY)

        embed.set_thumbnail(url=user.avatar_url)
        embed.add_field(name="User ID", value=user.id)
        embed.add_field(name="Unban Timestamp",
                        value=HuskyUtils.get_timestamp())

        LOG.info(f"User {user} was unbanned from {guild.name}.")
        await alert_channel.send(embed=embed)
Ejemplo n.º 8
0
    async def on_raw_reaction_clear(self,
                                    event: discord.RawReactionClearEvent):
        channel = self.bot.get_channel(
            event.channel_id)  # type: discord.TextChannel
        message = await channel.fetch_message(event.message_id
                                              )  # type: discord.Message

        channel_config = self._config.get('reactToPin', {}).get(
            str(channel.id))  # type: dict

        if not HuskyUtils.should_process_message(message):
            return

        if message.id in channel_config.get('permanent', []):
            LOG.info(
                "Reactions were cleared on a permanently pinned message, ignoring."
            )
            return

        # Check if the message is pinned
        if message not in await channel.pins():
            LOG.debug("Can't unpin a message that isn't currently pinned.")
            return

        await message.unpin()
Ejemplo n.º 9
0
    async def convert(self, ctx: commands.Context, argument: str):
        if argument in ["0", "perm", "permanent", "inf", "infinite", "-"]:
            return None

        try:
            return HuskyUtils.get_timedelta_from_string(argument)
        except ValueError as e:
            raise commands.BadArgument(str(e))
Ejemplo n.º 10
0
    async def user_rename_logger(self, before: discord.Member,
                                 after: discord.Member):
        if "userRename" not in self._config.get("loggers", {}).keys():
            return

        logger_ignores: dict = self._session_store.get('loggerIgnores', {})
        ignored_nicks = logger_ignores.setdefault('nickname', [])

        alert_channel = self._config.get('specialChannels',
                                         {}).get(ChannelKeys.USER_LOG.value,
                                                 None)

        if alert_channel is None:
            return

        alert_channel = self.bot.get_channel(alert_channel)

        if before.nick == after.nick and before.name == after.name:
            return

        if before.nick != after.nick:
            update_type = 'nickname'
            old_val = before.nick
            new_val = after.nick

            if before.id in ignored_nicks:
                return

        elif before.name != after.name:
            update_type = 'username'
            old_val = before.name
            new_val = after.name
        else:
            return

        embed = discord.Embed(
            description=
            f"User's {update_type} has been changed. Information below.",
            color=Colors.INFO)

        embed.add_field(name=f"Old {update_type.capitalize()}",
                        value=old_val,
                        inline=True)
        embed.add_field(name=f"New {update_type.capitalize()}",
                        value=new_val,
                        inline=True)
        embed.add_field(name="Display Name",
                        value=HuskyUtils.escape_markdown(after.display_name),
                        inline=True)
        embed.add_field(name="User ID", value=after.id, inline=True)
        embed.set_author(name=f"{after}'s {update_type} has changed!",
                         icon_url=after.avatar_url)

        await alert_channel.send(embed=embed)
Ejemplo n.º 11
0
    async def user_filter(self, message: discord.Message):
        flag_users = self._config.get("flaggedUsers", [])

        alert_channel = self._config.get('specialChannels', {}).get(
            ChannelKeys.STAFF_ALERTS.value, None)
        if alert_channel is not None:
            alert_channel: discord.TextChannel = self.bot.get_channel(
                alert_channel)

        if not HuskyUtils.should_process_message(message):
            return

        if message.author.id in flag_users:
            embed = discord.Embed(
                title=Emojis.RED_FLAG + " Message autoflag raised!",
                description=
                f"A message from flagged user {message.author.mention} was detected and has been raised to "
                f"staff. Please investigate.",
                color=Colors.WARNING)

            embed.add_field(name="Message Content",
                            value=HuskyUtils.trim_string(
                                message.content, 1000),
                            inline=False)
            embed.add_field(name="Message ID", value=message.id, inline=True)
            embed.add_field(name="Channel",
                            value=message.channel.mention,
                            inline=True)
            embed.add_field(name="User ID",
                            value=message.author.id,
                            inline=True)
            embed.add_field(name="Message Timestamp",
                            value=message.created_at.strftime(DATETIME_FORMAT),
                            inline=True)

            if alert_channel is not None:
                await alert_channel.send(embed=embed,
                                         delete_after=self._delete_time)

            LOG.info("Got user flagged message (from %s in %s): %s",
                     message.author, message.channel, message.content)
Ejemplo n.º 12
0
        def undersized_gif_check(file) -> bool:
            # Try to see if this gif is too big for its size (over 5000px^2, but less than 1mb)
            (width, height) = HuskyUtils.get_image_size(file.name)

            if (width > 5000) and (height > 5000) and os.path.getsize(
                    file.name) < 1000000:
                LOG.info(
                    "Found a GIF that exceeds sane size limits (over 5000px^2, but under 1mb)"
                )
                return True

            return False
Ejemplo n.º 13
0
    async def on_ping(self, message: discord.Message):
        if not HuskyUtils.should_process_message(message):
            return

        if message.content.startswith(self._bot.command_prefix):
            return

        # hacky way to determine if a message is only a bot mention
        if message.clean_content == f"@{message.guild.me.display_name}":
            return

        if self._bot.user in message.mentions:
            await message.channel.send(self.husky_speak())
Ejemplo n.º 14
0
    async def generate_flag(self, ctx: commands.Context):
        """
        When necessary, this command allows the bot to generate a new flag file for internal use. This command will
        invalidate the existing flag file entirely.
        """
        self.generate_ctf_file(True)
        fh = HuskyUtils.get_sha1_hash_of_file(CTF_PATH)

        await ctx.send(embed=discord.Embed(
            title=Emojis.RED_FLAG + " HuskyBot CTF Challenge",
            description=
            f"The flag file has been generated. The file's hash is `{fh[-8:]}`.",
            color=Colors.SUCCESS))
Ejemplo n.º 15
0
    async def filter_message(self, message: discord.Message, context: str = "new_message"):
        if not HuskyUtils.should_process_message(message):
            return

        if message.author.permissions_in(message.channel).manage_messages:
            return

        for ubl_term in self.bot.config.get('ubl', {}).get('bannedPhrases', []):
            if re.search(ubl_term, message.content, re.IGNORECASE) is not None:
                await message.author.ban(reason=f"User used UBL keyword `{ubl_term}`. Purging user...",
                                         delete_message_days=5)
                await message.guild.unban(message.author, reason="UBL ban reversal")
                LOG.info("Kicked UBL triggering user (context %s, keyword %s, from %s in %s): %s", context,
                         message.author, ubl_term, message.channel, message.content)
Ejemplo n.º 16
0
    async def log(self, ctx: discord.ext.commands.Context, lines: int = 10):
        """
        Extract a segment of the bot's current log file.

        This command takes an optional parameter (lines) which can be used to seek back in the bot's log file by a
        certain number of lines.

        This command has a limited output of 2000 characters, so the log may be trimmed. This allows for administrators
        to creatively abuse the lines function to get basic pagination.

        WARNING: The log command may reveal some sensitive information about bot execution!

        Parameters
        ----------
            ctx    :: Discord context <!nodoc>
            lines  :: The number of lines to pull from the log file.
        """

        log_file = self._session_store.get('logPath')

        if log_file is None:
            await ctx.send(embed=discord.Embed(
                title="Bot Manager",
                description=
                "A log file was expected, but was not found or configured. This suggests a *serious* "
                "problem with the bot.",
                color=Colors.DANGER))
            return

        logs = HuskyUtils.tail(log_file, lines)

        log_title = f"**Log Entries from {log_file}**"
        log_data = HuskyUtils.trim_string(
            logs.replace('```', '`\u200b`\u200b`'),
            2000 - (len(log_title) + 15), True)

        await ctx.send(log_title + "\n" + f"```{log_data}```")
Ejemplo n.º 17
0
    async def on_raw_reaction_remove(self,
                                     payload: discord.RawReactionActionEvent):
        channel = self.bot.get_channel(
            payload.channel_id)  # type: discord.TextChannel
        message = await channel.fetch_message(payload.message_id
                                              )  # type: discord.Message

        channel_config = self._config.get('reactToPin', {}).get(
            str(channel.id))  # type: dict

        if not HuskyUtils.should_process_message(message):
            return

        if channel_config is None or not channel_config.get('enabled', False):
            LOG.debug(
                f"A pin configuration was not found for channel {channel}. Ignoring message."
            )
            return

        # Check if the message is pinned
        if message not in await channel.pins():
            LOG.debug("Can't unpin a message that isn't currently pinned.")
            return

        if str(payload.emoji) != channel_config.get('emoji'):
            LOG.debug(
                f"Got an invalid emoji for message {message.id} in channel {channel}, ignoring."
            )
            return

        if message.id in channel_config.get('permanent', []):
            LOG.info(
                "Reactions dropped below threshold on permanently pinned message, ignoring but logging."
            )
            return

        # we are in a valid channel now, with a valid emote.
        current_reactions = await self.count_reactions(message, payload.emoji)

        if current_reactions >= channel_config.get('requiredToPin', 6):
            LOG.debug(
                "Got a valid removal event for the emote, but there are too many reactions to unpin."
            )
            return

        await message.unpin()
        LOG.info(
            f"Unpinned previously pinned message {message.id} in {channel}, as it is no longer at the required "
            f"reaction count.")
Ejemplo n.º 18
0
    async def start_giveaway(self, ctx: commands.Context, title: str, end_time: datetime.datetime,
                             winners: int) -> HuskyData.GiveawayObject:

        """
        Begin a new Giveaway.

        This command will build a new Giveaway, register it with the event loop and cache, and store it in the
        persistent file. If a giveaway is to be executed, it ***must*** be created with this method.

        :param ctx: The Context responsible for creating the giveaway.
        :param title: The title/name of the giveaway (usually the object people will win)
        :param end_time: A DateTime to end the giveaway
        :param winners: A number of winners (greater than zero) to choose from.
        :return: Returns the created GiveawayObject
        """
        channel = ctx.channel

        if winners == 1:
            winner_str = "1 winner"
        else:
            winner_str = f"{winners} winners"

        giveaway_embed = discord.Embed(
            title=f"{Emojis.GIVEAWAY} New Giveaway: {title}!",
            description=f"A giveaway has been started for **{title}**!\n\nAnyone may enter, and up to {winner_str} "
                        f"will be selected for the final prize. React with the {Emojis.GIVEAWAY} emoji to enter."
                        f"\n\nThis giveaway will end at {end_time.strftime(DATETIME_FORMAT)}",
            color=Colors.INFO
        )

        message = await ctx.send(embed=giveaway_embed)
        await message.add_reaction(Emojis.GIVEAWAY)

        giveaway = HuskyData.GiveawayObject()
        giveaway.name = title
        giveaway.end_time = end_time.timestamp()  # All timestamps are UTC.
        giveaway.winner_count = winners
        giveaway.register_channel_id = channel.id
        giveaway.register_message_id = message.id

        pos = HuskyUtils.get_sort_index(self.__cache__, giveaway, 'end_time')

        # note, we insert the giveaway, and sort. this is a rare operation, so a sort is "acceptable"
        # Null-ending giveaways (usually impossible) will be placed at the very end.
        self.__cache__.insert(pos, giveaway)
        self.__cache__.sort(key=lambda g: g.end_time if g.end_time else 10 * 100)
        self._giveaway_config.set(GIVEAWAY_CONFIG_KEY, self.__cache__)

        return giveaway
Ejemplo n.º 19
0
    async def on_ping(self, message: discord.Message):
        if not HuskyUtils.should_process_message(message):
            return

        if message.content.startswith(self._bot.command_prefix):
            return

        bot_mention = f"<@{self._bot.user.id}>"

        # Don't let people ping Husky to get a response.
        if bot_mention == message.content:
            return

        if bot_mention in message.content:
            await message.channel.send(self.husky_speak())
Ejemplo n.º 20
0
    async def on_message(self, message: discord.Message):
        if not HuskyUtils.should_process_message(message):
            return

        if message.author.id in self._config.get('userBlacklist', []):
            return

        if message.channel.id in self._config.get('disabledChannels', []) \
                and isinstance(message.author, discord.Member) \
                and not message.author.permissions_in(message.channel).manage_messages:
            return

        if self._session_store.get('lockdown', False):
            return

        responses = self._config.get("responses", {})

        for response in responses.keys():
            if not (message.content.lower().startswith(response.lower())):
                continue

            if not ((responses[response].get('allowedChannels') is None) or
                    (message.channel.id
                     in responses[response].get('allowedChannels'))):
                continue

            if HuskyUtils.member_has_any_role(message.author, responses[response].get('requiredRoles')) \
                    or bool(message.author.permissions_in(message.channel).manage_messages):
                if responses[response].get('isEmbed', False):
                    await message.channel.send(
                        content=None,
                        embed=discord.Embed.from_dict(
                            responses[response]['response']))
                else:
                    await message.channel.send(
                        content=responses[response]['response'])
Ejemplo n.º 21
0
    async def requestify(self, ctx: commands.Context, url: str, method: str = "GET", *, data: str = None):
        """
        Make an HTTP call to an [external] server.

        This command functionally acts as cURL, and allows for HTTP calls to be made and sent to a server. This command
        may hit HuskyBot's internal API server.

        The following HTTP methods are supported: GET, POST, PUT, DELETE, PATCH

        The bot API server is available at http://127.0.0.1 at whatever port is defined (default 9339).
        """
        method = method.upper()
        supported_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"]

        if method not in supported_methods:
            await ctx.send(embed=discord.Embed(
                title="Invalid request method!",
                description="Only the following request methods are supported:\n\n"
                            "{}".format(', '.join('`{}`'.format(m) for m in supported_methods)),
                color=Colors.ERROR
            ))
            return

        try:
            async with aiohttp.client.request(method, url, data=data) as response:
                if 100 <= response.status <= 199:
                    color = Colors.INFO
                elif 200 <= response.status <= 299:
                    color = Colors.SUCCESS
                elif 300 <= response.status <= 399:
                    color = Colors.WARNING
                else:
                    color = Colors.DANGER

                await ctx.send(embed=discord.Embed(
                    title=f"HTTP Status {response.status}",
                    description="```{}```".format(HuskyUtils.trim_string(await response.text(), 2000)),
                    color=color
                ))
        except aiohttp.client.ClientError as ex:
            await ctx.send(embed=discord.Embed(
                title="Could Not Make Request",
                description=f"Requestify failed to make a request due to error `{type(ex).__name__}`. "
                            f"Data has been logged.",
                color=Colors.DANGER
            ))
            LOG.warning("Requestify raised exception.", ex)
Ejemplo n.º 22
0
    def entrypoint(self):
        if os.environ.get('DISCORD_TOKEN'):
            LOG.debug(
                "Loading API key from environment variable DISCORD_TOKEN.")
        elif self.config.get('apiKey'):
            LOG.warning(
                "DEPRECATION WARNING - The Discord API key is being retrieved from the config file. "
                "This capability will be removed in a future release. Please move the token to the "
                "DISCORD_TOKEN environment variable.")
        else:
            LOG.critical(
                "The API key for HuskyBot must be loaded via the DISCORD_TOKEN environment variable."
            )
            exit(1)

        LOG.info("The bot's log path is: {}".format(self.__log_path))

        if HuskyUtils.is_docker():
            LOG.info(
                "The bot has detected it is running in Docker. Some internal systems have been changed in order "
                "to better suit the container land.")

        if self.__daemon_mode:
            LOG.info(
                "The bot is currently loaded in Daemon Mode. In Daemon Mode, certain functionalities are "
                "slightly altered to better utilize the headless environment.")

        if self.developer_mode:
            LOG.info(
                "The bot is running in DEVELOPER MODE! Some features may behave in unexpected ways or may "
                "otherwise break. Some bot safety checks are disabled with this mode on."
            )

        self.run(os.getenv('DISCORD_TOKEN', self.config.get('apiKey')))

        LOG.info("Shutting down HuskyBot...")

        self.config.save()
        LOG.debug("Config file saved/written to disk.")

        if self.db:
            self.db.dispose()
            LOG.debug("DB connection shut down")

        if self.config.get("restartReason") is not None:
            LOG.info("Bot is ready for restart...")
            os.execl(sys.executable, *([sys.executable] + sys.argv))
Ejemplo n.º 23
0
    def generate_ctf_file(self, force: bool = False):
        ctf_config = self._config.get('ctf', {})
        if os.path.isfile(CTF_PATH) and not force:
            LOG.debug("A CTF file already exists, so not regenerating.")
            return

        with open(CTF_PATH, 'w') as f:
            f.write(
                FLAG_TEMPLATE.format(flag_key=str(uuid.uuid4()),
                                     ts=HuskyUtils.get_timestamp(),
                                     salt=secrets.token_urlsafe(64)))

        ctf_config['pwned_by'] = None
        ctf_config['pwned_at'] = None
        self._config.set('ctf', ctf_config)

        LOG.info("Generated a CTF flag file!")
Ejemplo n.º 24
0
    async def on_error(self, event_method, *args, **kwargs):
        exception = sys.exc_info()

        channel = self.config.get('specialChannels',
                                  {}).get(ChannelKeys.STAFF_LOG.value, None)

        if channel is None:
            LOG.warning(
                'A logging channel is not set up! Error messages will not be forwarded to '
                'Discord.')
            raise exception

        channel = self.get_channel(channel)

        if isinstance(exception,
                      discord.HTTPException) and exception.code == 502:
            LOG.error(
                f"Got HTTP status code {exception.code} for method {event_method} - Discord is "
                f"likely borked now.")
        else:
            LOG.error('Exception in method %s:\n%s', event_method,
                      traceback.format_exc())

            try:
                embed = discord.Embed(
                    title="Bot Exception Handler",
                    description="Exception in method `{}`:\n```{}```".format(
                        event_method,
                        HuskyUtils.trim_string(
                            traceback.format_exc().replace(
                                '```', '`\u200b`\u200b`'), 1500)),
                    color=Colors.DANGER)

                owner_id = self.session_store.get('appInfo', None).owner.id
                dev_ping = self.config.get("specialRoles", {}).get(
                    SpecialRoleKeys.BOT_DEVS.value, owner_id)

                await channel.send(
                    "<@{}>, an error has occurred with the bot. See attached "
                    "embed.".format(dev_ping),
                    embed=embed)
            except Exception as e:
                LOG.critical(
                    "There was an error sending an error to the error channel.\n "
                    + str(e))
                raise e
Ejemplo n.º 25
0
    async def user_ban_logger(self, guild: discord.Guild, user: discord.User):
        if "userBan" not in self._config.get("loggers", {}).keys():
            return

        logger_ignores: dict = self._session_store.get('loggerIgnores', {})
        ignored_bans = logger_ignores.setdefault('ban', [])

        if user.id in ignored_bans:
            return

        # Get timestamp as soon as the event is fired, because waiting for bans may take a while.
        timestamp = HuskyUtils.get_timestamp()

        alert_channel = self._config.get('specialChannels',
                                         {}).get(ChannelKeys.STAFF_LOG.value,
                                                 None)

        if alert_channel is None:
            return

        alert_channel = self.bot.get_channel(alert_channel)

        embed = discord.Embed(title=Emojis.BAN + " User banned",
                              description=f"{user} was banned from the guild.",
                              color=Colors.DANGER)

        ban_entry = discord.utils.get(await guild.bans(), user=user)

        if ban_entry is None:
            raise ValueError(
                f"A ban record for user {user.id} was expected, but no entry was found"
            )

        ban_reason = ban_entry.reason

        if ban_reason is None:
            ban_reason = "<No ban reason provided>"

        embed.set_thumbnail(url=user.avatar_url)
        embed.add_field(name="User ID", value=user.id, inline=True)
        embed.add_field(name="Ban Timestamp", value=timestamp, inline=True)
        embed.add_field(name="Ban Reason", value=ban_reason, inline=False)

        LOG.info(
            f"User {user} was banned from {guild.name} for '{ban_reason}'.")
        await alert_channel.send(embed=embed)
Ejemplo n.º 26
0
    async def requestify(self,
                         ctx: commands.Context,
                         url: str,
                         method: str = "GET",
                         *,
                         data: str = None):
        method = method.upper()
        supported_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"]

        if method not in supported_methods:
            await ctx.send(embed=discord.Embed(
                title="Invalid request method!",
                description=
                "Only the following request methods are supported:\n\n"
                "{}".format(', '.join('`{}`'.format(m)
                                      for m in supported_methods)),
                color=Colors.ERROR))
            return

        try:
            async with aiohttp.client.request(method, url,
                                              data=data) as response:
                if 100 <= response.status <= 199:
                    color = Colors.INFO
                elif 200 <= response.status <= 299:
                    color = Colors.SUCCESS
                elif 300 <= response.status <= 399:
                    color = Colors.WARNING
                else:
                    color = Colors.DANGER

                await ctx.send(embed=discord.Embed(
                    title=f"HTTP Status {response.status}",
                    description="```{}```".format(
                        HuskyUtils.trim_string(await response.text(), 2000)),
                    color=color))
        except aiohttp.client.ClientError as ex:
            await ctx.send(embed=discord.Embed(
                title="Could Not Make Request",
                description=
                f"Requestify failed to make a request due to error `{type(ex).__name__}`. "
                f"Data has been logged.",
                color=Colors.DANGER))
            LOG.warning("Requestify raised exception.", ex)
Ejemplo n.º 27
0
    async def get_file_hash(self, ctx: commands.Context):
        """
        This command will expose the SHA1 hash of the CTF flag file, and not much else.

        For information about what's in the flag file, do `/help CTFChallenge`.
        """
        if not os.path.isfile(CTF_PATH):
            await ctx.send(embed=discord.Embed(
                title=Emojis.RED_FLAG + " HuskyBot CTF Challenge",
                description=
                f"A CTF flag file does not currently exist. Please contact a bot admin.",
                color=Colors.DANGER))
            return

        fhash = HuskyUtils.get_sha1_hash_of_file(CTF_PATH)

        await ctx.send(embed=discord.Embed(
            title=Emojis.RED_FLAG + " HuskyBot CTF Challenge",
            description=f"The flag file's SHA1 hash is: \n```{fhash}```",
            color=Colors.SUCCESS))
Ejemplo n.º 28
0
    async def on_message(self, message: discord.Message):
        author = message.author
        if not HuskyUtils.should_process_message(message):
            return

        if message.content.startswith(self.command_prefix):
            if (author.id in self.config.get('userBlacklist',
                                             [])) and (author.id
                                                       not in self.superusers):
                LOG.info("Blacklisted user %s attempted to run command %s",
                         message.author, message.content)
                return

            if message.content.lower().split(' ')[0][1:] in self.config.get(
                    'ignoredCommands', []):
                LOG.info("User %s ran an ignored command %s", message.author,
                         message.content)
                return

            if message.content.lower().split(' ')[0].startswith('/r/'):
                LOG.info("User %s linked to subreddit %s, ignoring command",
                         message.author, message.content)
                return

            if self.session_store.get(
                    'lockdown', False) and (author.id not in self.superusers):
                LOG.info(
                    "Lockdown mode is enabled for the bot. Command blocked.")
                return

            if message.channel.id in self.config.get("disabledChannels", []) and isinstance(author, discord.Member) \
                    and not author.permissions_in(message.channel).manage_messages:
                LOG.info(
                    f"Got a command from a disabled channel {message.channel}. Command blocked."
                )
                return

            LOG.info("User %s ran %s", author, message.content)

            await self.process_commands(message)
Ejemplo n.º 29
0
        def generate_cleanup_filter():
            if filter_def is None:
                return None

            content_list = filter_def.split('--')

            # Filter types
            regex_list = []
            user_list = []

            for filter_candidate in content_list:
                if filter_candidate is None or filter_candidate == '':
                    continue

                filter_candidate = filter_candidate.strip()
                filter_candidate = filter_candidate.split(" ", 1)

                if filter_candidate[0] in ["user", "author", "member"]:
                    user_id = HuskyUtils.get_user_id_from_arbitrary_str(
                        ctx.guild, filter_candidate[1])
                    user_list.append(user_id)
                elif filter_candidate[0] in ["regex"]:
                    regex_list.append(filter_candidate[1])
                else:
                    raise KeyError(
                        f"Filter {filter_candidate[0]} is not valid!")

            def dynamic_check(message: discord.Message):
                if len(user_list) > 0 and message.author.id not in user_list:
                    return False

                for regex in regex_list:
                    if len(regex_list) > 0 and re.search(
                            regex, message.content) is None:
                        return False

                return True

            return dynamic_check
Ejemplo n.º 30
0
    async def user_leave_logger(self, member: discord.Member):
        if "userLeave" not in self._config.get("loggers", {}).keys():
            return

        alert_channel = self._config.get('specialChannels',
                                         {}).get(ChannelKeys.USER_LOG.value,
                                                 None)

        if alert_channel is None:
            LOG.debug("skipping log event - no userlog channel set.")
            return

        alert_channel = member.guild.get_channel(alert_channel)

        embed = discord.Embed(title=Emojis.DOOR + " Member left the guild",
                              description=f"{member} has left the guild.",
                              color=Colors.WARNING)

        embed.set_thumbnail(url=member.avatar_url)
        embed.add_field(name="User ID", value=member.id)
        embed.add_field(name="Leave Timestamp",
                        value=HuskyUtils.get_timestamp())

        roles_on_leave = []
        for role in member.roles:  # type: discord.Role
            if role.is_default():
                continue

            roles_on_leave.append(role.mention)

        if roles_on_leave:
            embed.add_field(name="Roles on Leave",
                            value=", ".join(roles_on_leave),
                            inline=False)

        LOG.info(f"User {member} has left {member.guild.name}.")
        await alert_channel.send(embed=embed)