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.")
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
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)
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)))
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)
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)