示例#1
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.")
示例#2
0
    def format_quote(self, quote: Quote, show_saved=True):
        s_fmt = "[{0}] <#{1}> <{2}> {3}" if self.cog_config.show_channel else "[{0}] <{2}> {3}"

        if self.cog_config.datetime_format == 'seconds':
            timestamp_str = format_datetime(quote.timestamp, seconds=True)
        elif self.cog_config.datetime_format == 'datetime':
            timestamp_str = format_datetime(quote.timestamp, seconds=False)
        elif self.cog_config.datetime_format == 'date':
            timestamp_str = format_date(quote.timestamp)
        else:
            raise RuntimeError("Invalid date_format??")

        s = s_fmt.format(timestamp_str, quote.channel_id, quote.author.mention,
                         quote.message)
        if show_saved:
            s += "\n*(saved by {})*".format(quote.saved_by.name)
        return s
示例#3
0
    async def check_in_report(self,
                              ctx: commands.Context,
                              *,
                              datespec: NaturalDateConverter = None):
        """!kazhelp
        description: "Get a report of who has or has not checked in in a given week."
        parameters:
            - name: datespec
              type: datespec
              optional: true
              default: 'last week ("7 days ago")'
              description: A date in any unambiguous format (2018-03-14, March 14 2018,
                  14 March 2018, today, 1 month ago, etc.). The report will be for the check-in week
                  that includes this date.
        examples:
            - command: .checkin report
              description: Get a report for last week.
            - command: .checkin report 2018-04-18
              description: Get a report for the week that includes 18 April 2018.
        """
        if not datespec:
            datespec = datetime.utcnow() - timedelta(days=7)

        start, end = self.c.get_check_in_week(datespec)
        week_str = "the week from {} to {}".format(format_datetime(start),
                                                   format_datetime(end))
        try:
            ci, nci = self.c.generate_check_in_report(
                datespec)  # checked in, not checked in
        except orm.exc.NoResultFound:
            await self.bot.say("No check-ins for {}.".format(week_str))
            return

        #
        # determine sorting order of each list
        #

        # checked in: by name
        ci_users = list(ci.keys())
        ci_users.sort(
            key=lambda u: u.nick.lower() if u.nick else u.name.lower())

        # not checked in: by last checkin date pre-reporting week
        nci_users = list(nci.keys())
        epoch = datetime(1970, 1, 1)
        nci_users.sort(key=lambda u: nci[u].timestamp
                       if nci.get(u, None) else epoch,
                       reverse=True)

        #
        # Prepare display
        #

        # format strings for display
        ci_list_str = '\n'.join("{0} ({1} - *{2:d} {3}*)".format(
            u.mention, format_datetime(ci[u].timestamp), ci[u].word_count,
            self.PROJECT_UNIT_MAP[ci[u].project_type]) for u in ci_users)
        nci_list_str = '\n'.join("{0} (last: {1})".format(
            u.mention,
            format_date(nci[u].timestamp) if nci.get(u, None) else 'Never')
                                 for u in nci_users)

        # Prepare the overall embed
        es = EmbedSplitter(title="Check-In Report",
                           colour=solarized.green,
                           description="Report for " + week_str,
                           timestamp=datetime.utcnow(),
                           repeat_header=True,
                           auto_truncate=True)
        es.set_footer(text="Generated: ")
        if len(ci_list_str) < Limits.EMBED_FIELD_VALUE:
            es.add_field(name="Checked in",
                         value=ci_list_str or 'Nobody',
                         inline=False)
        else:
            es.add_field(name="Checked in",
                         value="{:d} users (list too long)".format(
                             len(ci_users)),
                         inline=False)

        es.add_field(name="Did NOT check in", value=nci_list_str, inline=False)
        await self.send_message(ctx.message.channel, embed=es)
示例#4
0
    async def check_in(self, ctx: commands.Context, word_count: NaturalInteger,
                       *, message: str):
        """!kazhelp
        brief: BLOTS weekly check-in.
        description: |
            BLOTS weekly check-in.

            Enter your **total** word (or page) count and a brief update message.

            If your project type is "words", enter your word_count in words (total). If your project
            type is "visual" or "script", enter your total number of pages instead. See also
            {{!checkin type}}.

            Check-ins are **only** allowed from {{checkin_window_start}} to {{checkin_window_end}},
            unless you are a mod or a member of the following roles: {{checkin_anytime_roles}}. The
            start and end of the checkin window are announced in the channel.
        parameters:
            - name: word_count
              type: number
              description: Your total word count (or total pages, depending on set project type).
                Do **not** include the word 'words' or 'pages'.
            - name: message
              type: string
              description: Your progress update. Maximum length 1000 characters.
        examples:
            - command: ".checkin 304882 Finished chapter 82 and developed some of the social and
                economic fallout of the Potato Battle of 1912."
        """

        # check if allowed to checkin at the current time
        msg_time = ctx.message.timestamp
        window = self.c.get_check_in_window(msg_time)
        is_in_window = window[0] <= msg_time <= window[1]
        is_anytime = set(ctx.message.author.roles) & set(
            self.checkin_anytime_roles)

        if not check_mod(ctx) and not is_in_window and not is_anytime:
            import calendar
            window_name = "from {0} {2} to {1} {2}".format(
                calendar.day_name[window[0].weekday()],
                calendar.day_name[window[1].weekday()],
                self.c.checkin_time.strftime('%H:%M') + ' UTC')
            raise UserInputError(
                "**You cannot check-in right now!** Check-ins are {}. Need help? Ask us in #meta!"
                .format(window_name))

        # validate argument
        word_count = word_count  # type: int  # for IDE type checking
        if word_count < 0:
            raise commands.BadArgument("word_count must be greater than 0.")
        if not message:
            raise commands.BadArgument("Check-in message is required.")

        # store the checkin
        check_in = self.c.save_check_in(member=ctx.message.author,
                                        word_count=word_count,
                                        message=message,
                                        timestamp=ctx.message.timestamp)
        start, end = self.c.get_check_in_week(ctx.message.timestamp)
        await self.bot.say(
            "{} Check-in for {:d} {} recorded for the week of {} to {}. Thanks!"
            .format(ctx.message.author.mention, check_in.word_count,
                    self.PROJECT_UNIT_MAP[check_in.project_type],
                    format_date(start), format_date(end)))
示例#5
0
    async def report(self,
                     ctx: commands.Context,
                     type_: str,
                     channel: str = None,
                     *,
                     daterange: DateRange = None):
        """
        [MOD ONLY] Generate and show a statistics report 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.

        The date range cannot cross the boundary of one month (because unique users are not tracked
        from month to month for anonymisation reasons; it's only possible to identify unique users
        within the same month).

        This will read and process the raw data to generate stats, 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:
        * type: One of "full", "weekday" or "hourly". "weekday" and "hourly" take the raw data and
            provide a breakdown by day of the week or hour of the day, respectively.
        * channel: The name of a channel on the server, or "all".
        * 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:
        .report full all 2018-01-12
        .report full all yesterday
        .report full #general 2018-01-12 to 2018-01-14
        .report weekday all 3 days ago to yesterday
        .report hourly #worldbuilding 2018-01-01 to 7 days ago
        """
        logger.debug("report: {}".format(message_log_str(ctx.message)))

        type_ = type_.lower()
        types = ["full", "weekday", "hourly"]
        if type_ not in types:
            raise commands.BadArgument(
                "Invalid type; types in {}".format(types))

        dates = daterange or self.default_daterange()

        if channel.lower() != 'all':
            conv = ChannelConverter(ctx, channel)
            channel = conv.convert()
        else:
            channel = None

        await self.bot.say("Preparing report, please wait...")

        if type_ == "full":
            try:
                report = reports.prepare_report(*dates, channel=channel)
            except ValueError as e:
                raise commands.BadArgument(e.args[0])
            if not channel:
                report.name = "Report for {} to {}"\
                    .format(format_date(dates[0]), format_date(dates[1]))
            else:  # channel
                report.name = "Report for #{} from {} to {}"\
                    .format(channel.name, format_date(dates[0]), format_date(dates[1]))
            await self.show_report(ctx.message.channel, report)
        elif type_ == "weekday":
            filename = self.report_file_format.format(
                type_, channel.name if channel is not None else 'all',
                core.format_filename_date(dates[0]),
                core.format_filename_date(dates[1]))

            try:
                week_reports = reports.prepare_weekday_report(*dates,
                                                              channel=channel)
            except ValueError as e:
                raise commands.BadArgument(e.args[0])

            heads = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
                     'Saturday', 'Sunday')
            with reports.collect_report_matrix(filename, week_reports,
                                               heads) as collect_file:
                logger.info("Sending collected reports file.")
                if channel:
                    msg = "Weekly report for {} from {} to {}"\
                        .format(channel.name, format_date(dates[0]), format_date(dates[1]))
                else:
                    msg = "Weekly report for {} to {}"\
                        .format(format_date(dates[0]), format_date(dates[1]))
                await self.bot.send_file(ctx.message.channel,
                                         collect_file,
                                         filename=filename,
                                         content=msg)
        elif type_ == "hourly":
            filename = self.report_file_format.format(
                type_, channel.name if channel is not None else 'all',
                core.format_filename_date(dates[0]),
                core.format_filename_date(dates[1]))

            try:
                hourly_reports = reports.prepare_hourly_report(*dates,
                                                               channel=channel)
            except ValueError as e:
                raise commands.BadArgument(e.args[0])

            heads = tuple(str(i) for i in range(24))
            with reports.collect_report_matrix(filename, hourly_reports,
                                               heads) as collect_file:
                logger.info("Sending collected reports file.")
                if channel:
                    msg = "Hourly report for {} from {} to {}" \
                        .format(channel.name, format_date(dates[0]), format_date(dates[1]))
                else:
                    msg = "Hourly report for {} to {}" \
                        .format(format_date(dates[0]), format_date(dates[1]))
                await self.bot.send_file(ctx.message.channel,
                                         collect_file,
                                         filename=filename,
                                         content=msg)
示例#6
0
    async def report(self, ctx: commands.Context, type_: str, channel: str,
                     *, daterange: DateRange=None):
        """!kazhelp
        description: |
            Generate and show a statistics report 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.

            The date range cannot cross the boundary of one month, as it is not possible to
            calculate per-user statistics across multiple months.

            This will read and process the raw data to generate stats, 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.
        parameters:
            - name: type
              type: '"full", "weekday" or "hourly"'
              description: Report type. "full" calculates overall stats; "weekday" generates stats
                for each day of the week (Monday, etc.); "hourly" generates stats for each hour
                of the day across the entire period.
            - name: channel
              type: string or "all"
              description: The name of a channel on the server, or "all".
            - name: daterange
              type: string
              optional: true
              description: The range of dates to generate the report from. Same format as in
                {{!userstats}}.
        examples:
            - command: .report full all 2018-01-12
            - command: .report full all yesterday
            - command: .report full #general 2018-01-12 to 2018-01-14
            - command: .report weekday all 3 days ago to yesterday
            - command: .report hourly #worldbuilding 2018-01-01 to 7 days ago
        """
        type_ = type_.lower()
        types = ["full", "weekday", "hourly"]
        if type_ not in types:
            raise commands.BadArgument("Invalid type; types in {}".format(types))

        dates = daterange or self.default_daterange()

        if channel.lower() != 'all':
            conv = ChannelConverter(ctx, channel)
            channel = conv.convert()
        else:
            channel = None

        await self.bot.say("Preparing report, please wait...")

        if type_ == "full":
            try:
                report = reports.prepare_report(*dates, channel=channel)
            except ValueError as e:
                raise commands.BadArgument(e.args[0])
            if not channel:
                report.name = "Report for {} to {}"\
                    .format(format_date(dates[0]), format_date(dates[1]))
            else:  # channel
                report.name = "Report for #{} from {} to {}"\
                    .format(channel.name, format_date(dates[0]), format_date(dates[1]))
            await self.show_report(ctx.message.channel, report)
        elif type_ == "weekday":
            filename = self.report_file_format.format(
                type_,
                channel.name if channel is not None else 'all',
                core.format_filename_date(dates[0]),
                core.format_filename_date(dates[1])
            )

            try:
                week_reports = reports.prepare_weekday_report(*dates, channel=channel)
            except ValueError as e:
                raise commands.BadArgument(e.args[0])

            heads = ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday')
            with reports.collect_report_matrix(filename, week_reports, heads) as collect_file:
                logger.info("Sending collected reports file.")
                if channel:
                    msg = "Weekly report for {} from {} to {}"\
                        .format(channel.name, format_date(dates[0]), format_date(dates[1]))
                else:
                    msg = "Weekly report for {} to {}"\
                        .format(format_date(dates[0]), format_date(dates[1]))
                await self.bot.send_file(
                    ctx.message.channel, collect_file, filename=filename, content=msg)
        elif type_ == "hourly":
            filename = self.report_file_format.format(
                type_,
                channel.name if channel is not None else 'all',
                core.format_filename_date(dates[0]),
                core.format_filename_date(dates[1])
            )

            try:
                hourly_reports = reports.prepare_hourly_report(*dates, channel=channel)
            except ValueError as e:
                raise commands.BadArgument(e.args[0])

            heads = tuple(str(i) for i in range(24))
            with reports.collect_report_matrix(filename, hourly_reports, heads) as collect_file:
                logger.info("Sending collected reports file.")
                if channel:
                    msg = "Hourly report for {} from {} to {}" \
                        .format(channel.name, format_date(dates[0]), format_date(dates[1]))
                else:
                    msg = "Hourly report for {} to {}" \
                        .format(format_date(dates[0]), format_date(dates[1]))
                await self.bot.send_file(
                    ctx.message.channel, collect_file, filename=filename, content=msg)