Exemple #1
0
    async def up(self, ctx):
        """
        [MOD ONLY] Colours a moderator's username.

        This command colours the moderator's username by applying a special role to it. This is
        used for moderators to be able to signal when they are speaking or intervening officially
        in their role as moderator.

        Arguments: None
        """
        logger.debug("up: {}".format(message_log_str(ctx.message)))

        for status_role_name, distinguish_role_name in self.distinguish_map.items(
        ):
            status_role = discord.utils.get(ctx.message.server.roles,
                                            name=status_role_name)
            if status_role and status_role in ctx.message.author.roles:
                distinguish_role = get_named_role(ctx.message.server,
                                                  distinguish_role_name)
                await self.bot.add_roles(ctx.message.author, distinguish_role)
                await self.bot.delete_message(ctx.message)
                logger.info("up: Gave {} the {} role".format(
                    ctx.message.author, distinguish_role_name))
                break
        else:
            err_msg = "up: user's roles not recognised: {}".format(
                message_log_str(ctx.message))
            logger.warning(err_msg)
            await self.bot.say(
                "That command is only available to mods and admins.")
            await self.send_output("[WARNING] " + err_msg)
Exemple #2
0
    async def roll(self, ctx, dice: str):
        """
        Rolls dice.

        Rolls a <sides>-sided die <num> times, and reports the rolls and total.

        Example: `.rolls 3d6` rolls three six-sided dice.
        """
        logger.info("roll: {}".format(message_log_str(ctx.message)))

        try:
            num_rolls, num_sides = map(int, dice.split('d'))
        except ValueError:
            err_msg = "Invalid format: {}".format(message_log_str(ctx.message))
            logger.warning("rolls(): " + err_msg)
            await self.bot.say('Invalid format. Please enter `.rolls XdY`, '
                               'where X and Y are positive whole numbers.')
            return

        if num_rolls <= 0:
            logger.warning("rolls(): arguments out of range")
            await self.bot.say("You have to roll at least 1 die.")
        elif num_sides <= 1:
            logger.warning("rolls(): arguments out of range")
            await self.bot.say("Dice must have at least 2 sides.")
        elif num_sides > 100 or num_rolls > 100:
            logger.warning("rolls(): arguments out of range")
            await self.bot.say(
                "The limit for dice number and sides is 100 each.")
        else:
            result = [random.randint(1, num_sides) for _ in range(num_rolls)]
            total = sum(result)
            await self.bot.say("{!s}\n**Sum:** {:d}".format(result, total))
            logger.info("Rolled dice: {:d}d{:d} = {!r} (sum={})".format(
                num_rolls, num_sides, result, total))
Exemple #3
0
    async def down(self, ctx):
        """
        [MOD ONLY] Uncolours a moderator's username.

        This command undoes the `.up` command.

        Arguments: None
        """
        logger.debug("down: {}".format(message_log_str(ctx.message)))

        for status_role_name, distinguish_role_name in self.distinguish_map.items(
        ):
            status_role = discord.utils.get(ctx.message.server.roles,
                                            name=status_role_name)
            if status_role and status_role in ctx.message.author.roles:
                distinguish_role = get_named_role(ctx.message.server,
                                                  distinguish_role_name)
                await self.bot.remove_roles(ctx.message.author,
                                            distinguish_role)
                await self.bot.delete_message(ctx.message)
                logger.info("down: Took away from {} the {} role".format(
                    ctx.message.author, distinguish_role_name))
                break
        else:
            err_msg = "down: user's roles not recognised: {}".format(
                message_log_str(ctx.message))
            logger.warning(err_msg)
            await self.bot.say(
                "That command is only available to mods and admins.")
            await self.send_output("[WARNING] " + err_msg)
Exemple #4
0
    async def on_error_notes(self, exc, ctx):
        cmd_string = message_log_str(ctx.message)

        if ctx is not None and ctx.command is not None:
            usage_str = get_usage_str(ctx)
        else:
            usage_str = '(Unable to retrieve usage information)'

        if isinstance(exc, commands.BadArgument):
            msg = "Bad argument passed in command: {}".format(cmd_string)
            logger.warning(msg)
            await self.bot.send_message(ctx.message.channel, (
                "Invalid argument(s) for the command `{}`. Did you mean `.notes add`?"
                "\n\n**Usage:** `{}`\n\nUse `{}` for help.").format(
                    get_command_str(ctx), usage_str, get_help_str(ctx)))
            # No need to log user errors to mods

        elif isinstance(exc, commands.TooManyArguments):
            msg = "Too many arguments passed in command: {}".format(cmd_string)
            logger.warning(msg)
            await self.bot.send_message(
                ctx.message.channel,
                ("Too many arguments. Did you mean `.notes add`?\n\n"
                 "**Usage:** `{}`\n\nUse `{}` for help.").format(
                     usage_str, get_help_str(ctx)))
        else:
            await self.on_error_query_user(exc, ctx)
Exemple #5
0
    async def select(self, ctx, list_index: int):
        """
        [MOD ONLY] Set a specific spotlight application as the currently selected application.

        Arguments:
        * list_index: Required. The numerical index of a spotlight application, as shown with
         .spotlight list.
        """
        logger.debug("set: {}".format(message_log_str(ctx.message)))
        self._load_applications()

        if not self.applications:
            logger.warning("set: No spotlight applications found")
            await self.bot.say("There are no spotlight applications!")
            return

        array_index = list_index - 1
        try:
            selected_app = await self._get_app(array_index)
        except IndexError:
            return  # already handled by _get_app
        else:
            self.current_app_index = array_index
            self._write_db()

            logger.info("set: Currently selected app: (#{:d}) {!s}"
                .format(list_index, selected_app))
            await self.send_spotlight_info(ctx.message.channel, selected_app)
            await self.send_validation_warnings(ctx, selected_app)
Exemple #6
0
    async def filter_switch(self, ctx):
        """
        [MOD ONLY] Change the bot output channel for wordfilter warnings.

        Switches between the configured filter warning channel and the general bot output channel
        (#mods and #bot_output at time of writing).
        """
        logger.info("switch: {}".format(message_log_str(ctx.message)))

        if self.dest_current is None and self.dest_warning is None:
            logger.warning("switch invoked before bot ready state???")
            await self.bot.say(
                "Sorry, I'm still booting up. Try again in a few seconds.")
            return

        if self.dest_current is self.dest_warning:
            self.dest_current = self.dest_output
        else:
            self.dest_current = self.dest_warning
        self.filter_cfg.set('filter', 'channel', str(self.dest_current.id))
        self.filter_cfg.write()

        logger.info("switch(): Changed filter warning channel to #{}".format(
            self.dest_current.name))
        await self.bot.say("Changed the filter warning channel to {}".format(
            self.dest_current.mention))
Exemple #7
0
    async def add(self, ctx, filter_type: str, word: str):
        """
        [MOD ONLY] Add a new filter word/expression.

        Arguments:
        * filter_type: The list to add to. One of ["warn", "del"] (shorthand: ["w", "d"])
        * word: The word or expression to match. USE QUOTATION MARKS AROUND IT IF MULTI-WORD.
          You can use '%' at the beginning or end of the expression to match word boundaries
          otherwise substring matching is done).

        Examples:

        `.filter add warn %word%` - Adds "word" (as an exact word match) to the auto-warning list.
        `.filter add del "%pink flamingo%"` - Add "pink flamingo" (exact expression) to the auto-
                delete list.
        `.filter a w %talk` - Shorthand. Add "%talk" to the warning list - this will match any words
                that start with "talk".
        """
        logger.info("add: {}".format(message_log_str(ctx.message)))
        validated_type = await self.validate_filter_type(filter_type)
        if validated_type is None:
            # error messages and logging already managed
            return
        else:
            # not a copy - can modify directly
            self.filter_cfg.get("filter", validated_type).append(word)
            self.filter_cfg.write()

            logger.info("add: {}: Added {!r} to the {} list.".format(
                ctx.message.author, word, validated_type))
            await self.bot.say("Added `{}` to the {} list.".format(
                word, validated_type))

            self._load_filter_rules()
Exemple #8
0
    async def quote_add(self, ctx: commands.Context, user: str, *,
                        message: str):
        """
        Add a new quote manually.

        You can use `.quote grab` instead to automatically grab a recent message.

        Arguments:
        * user: Required. The user to find a quote for. See `.help quote` for valid formats.
        * message: Required. The quote text to add.

        Examples:
            .quote add @JaneDoe Ready for the mosh pit, shaka brah.
        """
        logger.info("quote add: {}".format(message_log_str(ctx.message)))
        if len(message) > Quote.MAX_MESSAGE_LEN:
            raise ValueError(
                "That quote is too long! Maximum length {:d} characters.".
                format(Quote.MAX_MESSAGE_LEN))
        quote = c.store_quote(user=c.query_user(self.server, user),
                              saved_by=c.query_user(self.server,
                                                    ctx.message.author.id),
                              channel_id=ctx.message.channel.id,
                              message=message,
                              timestamp=ctx.message.timestamp)

        message = "Added quote: {}".format(self.format_quote(quote))
        logger.info(message)
        await self.bot.say(
            embed=self.make_single_embed(quote, title="Added quote."))
        await self.send_output(message)
Exemple #9
0
    async def quote_list(self,
                         ctx: commands.Context,
                         user: str,
                         page: int = None):
        """
        Retrieve a list of quotes. Reply is always PMed.

        Arguments:
        * user: Required. The user to find a quote for. See `.help quote` for valid formats.
        * page: Optional. The page number to access, if there are more than 1 pages of notes.
          Default: last page.

        Examples:
            .quote list @JaneDoe - List all quotes by JaneDoe (page 1 if multiple pages)..
            .quote list @JaneDoe 4 - List the 4th page of quotes by JaneDoe.
        """
        logger.info("quote list: {}".format(message_log_str(ctx.message)))
        db_user = c.query_user(self.server, user)
        db_records = c.query_author_quotes(db_user)
        paginator = Pagination(db_records,
                               self.QUOTES_PER_PAGE,
                               align_end=True)
        if page is not None:
            paginator.page = max(0, min(paginator.total_pages - 1, page - 1))
        await self.send_quotes_list(ctx.message.author, paginator, db_user,
                                    ctx.message.server)
Exemple #10
0
    async def quote_find(self,
                         ctx: commands.Context,
                         user: str,
                         *,
                         search: str = None):
        """
        Find the most recent quote matching a user and/or text search.

        Arguments:
        * user: Required. The user to find a quote for, or part of their name or nickname to search,
            or "all". For exact user matches, see `.help quote` for valid formats.
        * search: Optional. Text to search in the quote.

        Examples:
            .quote find Jane - Find a quote for a user whose user/nickname contains "Jane".
            .quote find @JaneDoe flamingo - Find a quote containing "flamingo" by JaneDoe.
            .quote find Jane flamingo - Find a quote matching user "Jane" and containing "flamingo".
        """
        logger.info("quote find: {}".format(message_log_str(ctx.message)))

        try:
            db_user = c.query_user(self.server, user)
        except ValueError:  # not a valid user ID format
            if user != 'all':
                db_user = c.search_users(user)
            else:
                db_user = None

        db_records = c.search_quotes(search, db_user)
        em = self.make_single_embed(db_records[-1])
        await self.bot.say(embed=em)
Exemple #11
0
 async def queue_next(self, ctx):
     """
     [MOD ONLY] Set the next spotlight in the queue as the currently selected spotlight, and
     remove it from the queue. This is useful when a new spotlight is ready to start, as you can
     then immediately use `.spotlight showcase` to announce it publicly.
     """
     logger.debug("queue next: {}".format(message_log_str(ctx.message)))
     self._load_applications()
     old_index = self.current_app_index
     self.current_app_index = self.queue_data.popleft()
     try:
         app = await self._get_current()
     except IndexError:
         self.bot.say("Sorry, the queued index seems to have become invalid!")
         self.queue_data.appendleft(self.current_app_index)
         self.current_app_index = old_index
         return  # get_current() already handles this
     except:
         self.queue_data.appendleft(self.current_app_index)
         self.current_app_index = old_index
         raise
     else:
         await self.send_spotlight_info(ctx.message.channel, app)
         await self.send_validation_warnings(ctx, app)
         self._write_db()
Exemple #12
0
    async def load(self, ctx: commands.Context, messages: int = 100):
        """!kazhelp
        description:
            Read the badge channel history and add any missing badges. Mostly useful for
            transitioning to bot-managed badges, or loading missed badges from bot downtime.
        parameters:
            - name: messages
              optional: true
              default: 100
              type: number
              description: Number of messages to read in history.
        """
        if messages <= 0:
            raise commands.BadArgument("`messages` must be positive")

        await self.bot.say(("Loading the last {:d} badge channel messages. "
                            "This might take a while...").format(messages))

        total_badges = 0
        async for message in self.bot.logs_from(self.channel, messages):
            if message.author.id == self.bot.user.id:
                continue
            logger.info("badges load: Attempting to add/update badge: " +
                        message_log_str(message))
            badge_row = await self.add_badge(message, suppress_errors=True)
            if badge_row:
                total_badges += 1

        await self.bot.say("Added or updated {:d} badges.".format(total_badges)
                           )
Exemple #13
0
    async def choose(self, ctx, *, choices: str):
        """
        Need some help making a decision? Let the bot choose for you!

        Arguments:
        * choices - Two or more choices, separated by commas `,`.

        Examples:
        `.choose a, b, c`
        """
        logger.info("choose: {}".format(message_log_str(ctx.message)))
        choices = list(map(str.strip, choices.split(",")))
        if "" in choices:
            logger.warning("choose(): argument empty")
            await self.bot.say("I cannot decide if there's an empty choice.")
        elif len(choices) < 2:
            logger.warning("choose(): arguments out of range")
            await self.bot.say("I need something to choose from.")
        elif len(choices) > self.MAX_CHOICES:
            logger.warning("choose(): arguments out of range")
            await self.bot.say("I don't know, that's too much to choose from! "
                               "I can't handle more than {:d} choices!".format(
                                   self.MAX_CHOICES))
        else:
            r = random.randint(0, len(choices) - 1)
            await self.bot.say(choices[r])
Exemple #14
0
    async def on_error_query_user(self, exc, ctx):
        cmd_string = message_log_str(ctx.message)
        if isinstance(exc, commands.CommandInvokeError):
            root_exc = exc.__cause__ if exc.__cause__ is not None else exc

            if isinstance(
                    root_exc, ValueError
            ) and root_exc.args and 'user ID' in root_exc.args[0]:
                logger.warning("Invalid user argument: {!s}. For {}".format(
                    root_exc, cmd_string))
                await self.bot.send_message(
                    ctx.message.channel,
                    "User format is not valid. User must be specified as an @mention, as a Discord "
                    "ID (numerical only), or a KazTron ID (`*` followed by a number)."
                )

            elif isinstance(root_exc, c.UserNotFound):
                logger.warning("User not found: {!s}. For {}".format(
                    root_exc, cmd_string))
                await self.bot.send_message(
                    ctx.message.channel,
                    "User was not found. The user must either exist in the KazTron modnotes "
                    "database already, or exist on Discord (for @mentions and Discord IDs)."
                )

            else:
                core_cog = self.bot.get_cog("CoreCog")
                await core_cog.on_command_error(exc, ctx, force=True
                                                )  # Other errors can bubble up
        else:
            core_cog = self.bot.get_cog("CoreCog")
            await core_cog.on_command_error(exc, ctx, force=True
                                            )  # Other errors can bubble up
Exemple #15
0
    async def group_add(self, ctx, user1: str, user2: str):
        """
        Group two users together.

        If one user is not in a group, that user is added to the other user's group. If both users
        are in separate groups, both groups are merged. This is irreversible.

        See `.help group` for more information on grouping.

        Arguments:
        * <user1> and <user2>: Required. The two users to link. See `.help notes`.

        Example:
        .notes group add @FireAlchemist#1234 @TinyMiniskirtEnthusiast#4444
        """
        logger.info("notes group: {}".format(message_log_str(ctx.message)))
        db_user1 = await c.query_user(self.bot, user1)
        db_user2 = await c.query_user(self.bot, user2)

        if db_user1.group_id is None or db_user1.group_id != db_user2.group_id:
            c.group_users(db_user1, db_user2)
            msg = "Grouped users {0} and {1}"
        else:
            msg = "Error: Users {0} and {1} are already in the same group!"

        await self.bot.say(
            msg.format(self.format_display_user(db_user1),
                       self.format_display_user(db_user2)))
Exemple #16
0
    async def rem(self, ctx, note_id: int):
        """

        [MOD ONLY] Remove an existing note.

        To prevent accidental data deletion, the removed note can be viewed and restored by admin
        users.

        Arguments:
        * <note_id>: Required. The ID of the note to remove. See `.help notes`.

        Example:

        .notes rem 122
            Remove note number 122.
        """
        logger.info("notes rem: {}".format(message_log_str(ctx.message)))
        try:
            record = c.mark_removed_record(note_id)
        except db.orm_exc.NoResultFound:
            await self.bot.say("Note ID {:04d} does not exist.".format(note_id)
                               )
        else:
            await self.show_records(ctx.message.channel,
                                    user=record.user,
                                    records=[record],
                                    box_title="Note removed.",
                                    page=None,
                                    short=True)
            await self.bot.send_message(self.ch_log,
                                        "Removed note #{:04d}".format(note_id))
Exemple #17
0
    async def removed(self, ctx, user: str, page: int = 1):
        """

        [ADMIN ONLY] Show deleted notes.

        Arguments:
        * <user>: Required. The user to filter by, or `all`. See `.help notes`.
        * page: Optional[int]. The page number to access, if there are more than 1 pages of notes.
          Default: 1.
        """
        logger.info("notes removed: {}".format(message_log_str(ctx.message)))
        if user != 'all':
            db_user = await c.query_user(self.bot, user)
            db_group = c.query_user_group(db_user)
            db_records = c.query_user_records(db_group, removed=True)
        else:
            db_user = None
            db_group = None
            db_records = c.query_user_records(None, removed=True)
        total_pages = int(math.ceil(len(db_records) / self.NOTES_PAGE_SIZE))
        page = max(1, min(total_pages, page))

        start_index = (page - 1) * self.NOTES_PAGE_SIZE
        end_index = start_index + self.NOTES_PAGE_SIZE

        await self.show_records(ctx.message.channel,
                                user=db_user,
                                records=db_records[start_index:end_index],
                                group=db_group,
                                page=page,
                                total_pages=total_pages,
                                total_records=len(db_records),
                                box_title='*** Removed Records',
                                short=True)
Exemple #18
0
    async def notes(self, ctx, user: str, page: int = 1):
        """
        [MOD ONLY] Access moderation logs.

        Arguments:
        * user: Required. The user for whom to find moderation notes. This can be an @mention, a
          Discord ID (numerical only), or a KazTron ID (starts with *).
        * page: Optional[int]. The page number to access, if there are more than 1 pages of notes.
          Default: 1.

        Example:
            .notes @User#1234
            .notes 330178495568436157 3
        """
        logger.info("notes: {}".format(message_log_str(ctx.message)))
        db_user = await c.query_user(self.bot, user)
        db_group = c.query_user_group(db_user)
        db_records = c.query_user_records(db_group)
        total_pages = int(math.ceil(len(db_records) / self.NOTES_PAGE_SIZE))
        page = max(1, min(total_pages, page))

        start_index = (page - 1) * self.NOTES_PAGE_SIZE
        end_index = start_index + self.NOTES_PAGE_SIZE

        await self.show_records(ctx.message.channel,
                                user=db_user,
                                records=db_records[start_index:end_index],
                                group=db_group,
                                page=page,
                                total_pages=total_pages,
                                total_records=len(db_records),
                                box_title='Moderation Record')
Exemple #19
0
    async def watches(self, ctx, page: int = 1):
        """
        [MOD ONLY] Show all watches currently in effect (i.e. non-expired watch, int, warn records).

        Arguments:
        * user: Required. The user for whom to find moderation notes. This can be an @mention, a
          Discord ID (numerical only), or a KazTron ID (starts with *).
        * page: Optional[int]. The page number to access, if there are more than 1 pages of notes.
          Default: 1.

        Example:
            .notes @User#1234
            .notes 330178495568436157 3
        """
        logger.info("notes watches: {}".format(message_log_str(ctx.message)))
        watch_types = (RecordType.watch, RecordType.int, RecordType.warn)
        db_records = c.query_unexpired_records(types=watch_types)
        total_pages = int(math.ceil(len(db_records) / self.NOTES_PAGE_SIZE))
        page = max(1, min(total_pages, page))

        start_index = (page - 1) * self.NOTES_PAGE_SIZE
        end_index = start_index + self.NOTES_PAGE_SIZE

        await self.show_records(ctx.message.channel,
                                user=None,
                                records=db_records[start_index:end_index],
                                page=page,
                                total_pages=total_pages,
                                total_records=len(db_records),
                                box_title='Active Watches',
                                short=True)
Exemple #20
0
    async def on_error_query_user(self, exc, ctx):
        cmd_string = message_log_str(ctx.message)
        if isinstance(exc, commands.CommandInvokeError):
            root_exc = exc.__cause__ if exc.__cause__ is not None else exc

            if isinstance(
                    root_exc, ValueError
            ) and root_exc.args and 'user ID' in root_exc.args[0]:
                logger.warning("Invalid user argument: {!s}. For {}".format(
                    root_exc, cmd_string))
                await self.bot.send_message(
                    ctx.message.channel,
                    "User format is not valid. User must be specified as an @mention or as a "
                    "Discord ID (numerical only).")

            elif isinstance(root_exc, c.UserNotFound):
                logger.warning("User not found: {!s}. For {}".format(
                    root_exc, cmd_string))
                await self.bot.send_message(ctx.message.channel,
                                            "No quotes found for that user.")

            elif isinstance(root_exc, orm.exc.NoResultFound):
                logger.warning("No quotes found: {!s}. For {}".format(
                    root_exc, cmd_string))
                await self.bot.send_message(ctx.message.channel,
                                            "No matching quotes found.")

            else:
                await self.core.on_command_error(
                    exc, ctx, force=True)  # Other errors can bubble up
        else:
            await self.core.on_command_error(exc, ctx, force=True
                                             )  # Other errors can bubble up
Exemple #21
0
    async def request(self, ctx, *, content: str):
        """
        Submit a bug report or feature request to the bot DevOps Team.

        Everyone can use this, but please make sure that your request is clear and has enough
        enough details. This is especially true for us to be able to track down and fix bugs:
        we need information like what were you trying to do, what did you expect to happen, what
        actually happened? Quote exact error messages and give dates/times).

        Please note that any submissions made via this system may be publicly tracked via the
        GitHub repo. By submitting a request via this system, you give us permission to post
        your username and message, verbatim or altered, to a public issue tracker for the purposes
        of bot development and project management.

        Abuse may be treated in the same way as other forms of spam on the Discord server.
        """
        logger.debug("request(): {}".format(message_log_str(ctx.message)))

        em = discord.Embed(color=0x80AAFF)
        em.set_author(name="User Issue Submission")
        em.add_field(name="User", value=ctx.message.author.mention, inline=True)
        try:
            em.add_field(name="Channel", value=ctx.message.channel.mention, inline=True)
        except AttributeError:  # probably a private channel
            em.add_field(name="Channel", value=ctx.message.channel, inline=True)
        em.add_field(name="Timestamp", value=format_timestamp(ctx.message), inline=True)
        em.add_field(name="Content", value=content, inline=False)
        await self.bot.send_message(self.ch_request, embed=em)
        await self.bot.say("Your issue was submitted to the bot DevOps team. "
                           "If you have any questions or if there's an urgent problem, "
                           "please feel free to contact the moderators.")
Exemple #22
0
    async def queue_add(self, ctx, *, daterange: str):
        """
        [MOD ONLY] Add a spotlight application scheduled for a given date range.

        The currently selected spotlight will be added. Use `.spotlight select` or `.spotlight roll`
        to change the currently selected spotlight.

        NOTE: KazTron will not take any action on the scheduled date. It is purely informational,
        intended for the bot operator, as well as determining the order of the queue.

        TIP: You can add the same Spotlight application to the queue multiple times (e.g. on
        different dates). To edit the date instead, use `.spotlight queue edit`.

        Arguments:
        * `daterange`: Required, string. A string in the form "date1 to date2". Each date can be
          in one of these formats:
            * An exact date: "2017-12-25", "25 December 2017", "December 25, 2017"
            * A partial date: "April 23"
            * A time expression: "tomorrow", "next week", "in 5 days". Does **not** accept days of
              the week ("next Tuesday").

        Examples:
        * `.spotlight queue add 2018-01-25 to 2018-01-26`
        * `.spotlight queue add april 3 to april 5`
        """
        logger.debug("queue add: {}".format(message_log_str(ctx.message)))
        self._load_applications()

        try:
            dates = parse_daterange(daterange)
        except ValueError as e:
            raise commands.BadArgument(e.args[0]) from e

        try:
            app = await self._get_current()
        except IndexError:
            return  # already handled by _get_current

        queue_item = {
            'index': self.current_app_index,
            'start': utctimestamp(dates[0]),
            'end': utctimestamp(dates[1])
        }
        self.queue_data.append(queue_item)
        logger.info("queue add: added #{:d} from current select at {} to {}"
            .format(self.current_app_index + 1, dates[0].isoformat(' '), dates[1].isoformat(' ')))

        self.sort_queue()
        queue_index = self.queue_data.index(queue_item)  # find the new position now
        self._write_db()
        start, end = self.format_date_range(dates[0], dates[1])
        await self.bot.say(self.QUEUE_CHANGED_FMT.format(
            msg=self.QUEUE_ADD_HEADING,
            i=queue_index+1,
            id=queue_item['index'] + 1,
            start=start,
            end=end,
            app=app.discord_str()
        ))
Exemple #23
0
    async def userstats(self,
                        ctx: commands.Context,
                        *,
                        daterange: DateRange = None):
        """
        [MOD ONLY] Retrieve a CSV dump of stats for a date or range of dates.

        If a range of dates is specified, the data retrieved is up to and EXCLUDING the second date.
        A day starts at midnight UTC.

        Note that if the range crosses month boundaries (e.g. March to April), then the unique user
        hashes can be correlated between each other only within a given month. The same user will
        have different hashes in different months. This is used as a anonymisation method, to avoid
        long-term tracking of a unique user.

        This will generate and upload a CSV file, and could take some time. Please avoid calling
        this function multiple times for the same data or requesting giant ranges.

        The file is compressed using gzip. Windows users should use a modern archiving programme
        like 7zip <https://www.7-zip.org/download.html>; macOS users can open these files
        natively. Linux users know the drill.

        Arguments:
        * daterange. Optional. This can be a single date (period of 24 hours), or a range of
          dates in the form `date1 to date2`. Each date can be specified as ISO format
          (2018-01-12), in English with or without abbreviations (12 Jan 2018), or as relative dates
          (5 days ago). Default is last month.

        Examples:
        .userstats 2018-01-12
        .userstats yesterday
        .userstats 2018-01-12 to 2018-01-14
        .userstats 3 days ago to yesterday
        .userstats 2018-01-01 to 7 days ago
        """
        logger.debug("userstats: {}".format(message_log_str(ctx.message)))

        dates = daterange or self.default_daterange()

        await self.bot.say(
            "One moment, collecting stats for {} to {}...".format(
                format_date(dates[0]), format_date(dates[1])))

        filename = self.output_file_format.format(
            core.format_filename_date(dates[0]),
            core.format_filename_date(dates[1]))
        with core.collect_stats(filename, dates[0], dates[1]) as collect_file:
            logger.info("Sending collected stats file.")
            await self.bot.send_file(ctx.message.channel,
                                     collect_file,
                                     filename=filename,
                                     content="User stats for {} to {}".format(
                                         format_date(dates[0]),
                                         format_date(dates[1])))

        if dates[1] >= utils.datetime.get_month_offset(self.last_report_dt, 1):
            self.bot.say(
                "**WARNING:** Data not yet anonymised - "
                "hashes on an unexpired salt are in use. Do not distribute.")
Exemple #24
0
    async def tempban(self,
                      ctx: commands.Context,
                      user: str,
                      *,
                      reason: str = ""):
        """
        [MOD ONLY] Tempban a user.

        This method will automatically create a modnote. It will not communicate with the user.

        This feature integrates with modnotes, and will automatically enforce "temp" notes, giving a
        role to users with unexpired "temp" notes and removing that role when the note expires. This
        command is shorthand for `.notes add <user> temp expires="[expires]" [Reason]`.

        **Arguments:**
        * user: The user to ban. See [modnotes: .notes](modnotes.html#1-notes) for more
          information.
        * expires=datespec: Optional. The datespec for the tempban's expiration. Use quotation
          marks if the datespec has spaces in it. See [modnotes: .notes add](modnotes.html#11-add)
          for more information on accepted syntaxes. Default is `expires="in 7 days"`.
        * reason: Optional, but highly recommended to specify. The reason to record in the
          modnote

        **Channels:** Mod and bot channels

        **Usable by:** Moderators only

        **Examples:**
        .tempban @BlitheringIdiot#1234 Was being a blithering idiot.
            Issues a 7-day ban.
        .tempban @BlitheringIdiot#1234 expires="in 3 days" Was being a slight blithering idiot only.
            Issues a 3-day ban.
        """
        logger.debug("tempban: {}".format(message_log_str(ctx.message)))

        if not self.cog_modnotes:
            raise RuntimeError("Can't find ModNotes cog")

        # Parse and validate kwargs (we won't use this, just want to validate valid keywords)
        try:
            kwargs, rem = parse_keyword_args(self.cog_modnotes.KW_EXPIRE,
                                             reason)
        except ValueError as e:
            raise commands.BadArgument(e.args[0]) from e
        else:
            if not kwargs:
                reason = 'expires="{}" {}'.format("in 7 days", reason)
            if not rem:
                reason += " No reason specified."

        # Write the note
        await ctx.invoke(self.cog_modnotes.add,
                         user,
                         'temp',
                         note_contents=reason)

        # Apply new mutes
        await self.update_tempbans()
Exemple #25
0
 async def delete_message(self, ctx: commands.Context):
     if not self.delete:
         return
     try:
         await self.bot.delete_message(ctx.message)
     except discord.Forbidden:
         logger.warning(("Cannot delete command message '{}': "
                         "forbidden (Discord permissions)").format(
                             message_log_str(ctx.message)[:256]))
Exemple #26
0
    async def on_message(self, message: discord.Message):
        """
        Message handler. Check all non-mod messages for filtered words.
        """
        is_mod = check_role(
            self.config.discord.get("mod_roles", []) +
            self.config.discord.get("admin_roles", []), message)
        is_pm = isinstance(message.channel, discord.PrivateChannel)
        if not is_mod and not is_pm:
            message_string = str(message.content)
            del_match = self.engines['delete'].check_message(message_string)
            warn_match = self.engines['warn'].check_message(message_string)

            # logging
            if del_match or warn_match:
                if del_match:
                    log_fmt = "Found filter match [auto-delete] '{1}' in {0}"
                else:  # is_warn
                    log_fmt = "Found filter match (auto-warn) '{2}' in {0}"

                logger.info(
                    log_fmt.format(message_log_str(message), del_match,
                                   warn_match))

            # delete
            if del_match:
                logger.debug("Deleting message")
                await self.bot.delete_message(message)

            # warn
            if del_match or warn_match:
                logger.debug("Preparing and sending filter warning")
                filter_type = 'delete' if del_match else 'warn'
                match_text = del_match if del_match else warn_match

                em = discord.Embed(color=self.match_warn_color[filter_type])
                em.set_author(name=self.match_headings[filter_type])
                em.add_field(name="User",
                             value=message.author.mention,
                             inline=True)
                em.add_field(name="Channel",
                             value=message.channel.mention,
                             inline=True)
                em.add_field(name="Timestamp",
                             value=format_timestamp(message),
                             inline=True)
                em.add_field(name="Match Text", value=match_text, inline=True)
                em.add_field(name="Message Link",
                             value='[Message link]({})'.format(
                                 get_jump_url(message)),
                             inline=True)
                em.add_field(name="Content",
                             value=natural_truncate(message_string,
                                                    Limits.EMBED_FIELD_VALUE),
                             inline=False)

                await self.bot.send_message(self.channel_current, embed=em)
Exemple #27
0
    async def process_answer(self, message: discord.Message):
        await self.purge()
        wiz_name, wizard = self.get_wizard_for(message.author)

        logger.info("Processing '{}' wizard answer for {}".format(
            wiz_name, message.author))
        logger.debug(message_log_str(message))

        wizard.answer(message.content)
Exemple #28
0
 async def on_error_group_add(self, exc, ctx):
     cmd_string = message_log_str(ctx.message)
     if isinstance(exc, commands.TooManyArguments):
         logger.warning("Too many args: {}".format(exc, cmd_string))
         await self.bot.send_message(
             ctx.message.channel, ctx.message.author.mention +
             " Too many parameters. Note that you can only `{}` two users at a time."
             .format(get_command_str(ctx)))
     else:
         await self.on_error_query_user(exc, ctx)
Exemple #29
0
 async def current(self, ctx):
     """ [MOD ONLY] Show the currently selected application. """
     logger.debug("current: {}".format(message_log_str(ctx.message)))
     self._load_applications()
     try:
         app = await self._get_current()
     except IndexError:
         return  # get_current() already handles this
     await self.send_spotlight_info(ctx.message.channel, app)
     await self.send_validation_warnings(ctx, app)
Exemple #30
0
 async def on_error_group_rem(self, exc, ctx):
     cmd_string = message_log_str(ctx.message)
     if isinstance(exc, commands.TooManyArguments):
         logger.warning("Too many args: {}".format(exc, cmd_string))
         await self.bot.send_message(
             ctx.message.channel, "Too many arguments. "
             "Note that you can only `{}` *one* user from its group at a time."
             .format(get_command_str(ctx)))
     else:
         await self.on_error_query_user(exc, ctx)