async def _heatmap( self, ctx: SlashContext, username: Optional[str] = "me", after: Optional[str] = None, before: Optional[str] = None, ) -> None: """Generate a heatmap for the given user.""" start = datetime.now() after_time, before_time, time_str = parse_time_constraints( after, before) msg = await ctx.send(i18n["heatmap"]["getting_heatmap"].format( user=get_initial_username(username, ctx), time_str=time_str)) utc_offset = extract_utc_offset(ctx.author.display_name) from_str = after_time.isoformat() if after_time else None until_str = before_time.isoformat() if before_time else None user = get_user(username, ctx, self.blossom_api) heatmap_response = self.blossom_api.get( "submission/heatmap/", params={ "completed_by": get_user_id(user), "utc_offset": utc_offset, "complete_time__gte": from_str, "complete_time__lte": until_str, }, ) if heatmap_response.status_code != 200: raise BlossomException(heatmap_response) data = heatmap_response.json() day_index = pd.Index(range(1, 8)) hour_index = pd.Index(range(0, 24)) heatmap = ( # Create a data frame from the data pd.DataFrame.from_records(data, columns=["day", "hour", "count"]) # Convert it into a table with the days as rows and hours as columns .pivot(index="day", columns="hour", values="count") # Add the missing days and hours .reindex(index=day_index, columns=hour_index)) heatmap_table = create_file_from_heatmap(heatmap, user, utc_offset) await msg.edit( content=i18n["heatmap"]["response_message"].format( user=get_username(user), time_str=time_str, duration=get_duration_str(start), ), file=heatmap_table, )
async def search( self, ctx: SlashContext, query: str, username: str = "me", after: Optional[str] = None, before: Optional[str] = None, ) -> None: """Search for transcriptions containing the given text.""" start = datetime.now() after_time, before_time, time_str = parse_time_constraints( after, before) # Send a first message to show that the bot is responsive. # We will edit this message later with the actual content. msg = await ctx.send(i18n["search"]["getting_search"].format( query=query, user=get_initial_username(username, ctx), time_str=time_str)) user = get_user(username, ctx, self.blossom_api) # Simulate an initial cache item cache_item: SearchCacheItem = { "query": query, "user": user, "after_time": after_time, "before_time": before_time, "time_str": time_str, "cur_page": 0, "discord_user_id": ctx.author_id, "response_data": None, "request_page": 0, } # Display the first page await self._search_from_cache(msg, start, cache_item, 0)
async def leaderboard( self, ctx: SlashContext, username: str = "me", after: Optional[str] = None, before: Optional[str] = None, ) -> None: """Get the leaderboard for the given user.""" start = datetime.now(tz=pytz.utc) after_time, before_time, time_str = parse_time_constraints(after, before) # Send a first message to show that the bot is responsive. # We will edit this message later with the actual content. msg = await ctx.send( i18n["leaderboard"]["getting_leaderboard"].format( user=get_initial_username(username, ctx), time_str=time_str ) ) user = get_user(username, ctx, self.blossom_api) top_count = 5 if user else 15 context_count = 5 from_str = after_time.isoformat() if after_time else None until_str = before_time.isoformat() if before_time else None # Get the leaderboard data leaderboard_response = self.blossom_api.get( "submission/leaderboard", params={ "user_id": get_user_id(user), "top_count": top_count, "below_count": context_count, "above_count": context_count, "complete_time__gte": from_str, "complete_time__lte": until_str, }, ) if leaderboard_response.status_code != 200: raise BlossomException(leaderboard_response) leaderboard = leaderboard_response.json() # Extract needed data top_users = leaderboard["top"] above_users = leaderboard["above"] lb_user = leaderboard["user"] below_users = leaderboard["below"] description = "" # Only show the top users if they are not already included top_user_limit = ( top_count + 1 if user is None else above_users[0]["rank"] if len(above_users) > 0 else lb_user["rank"] ) # Show top users for top_user in top_users[: top_user_limit - 1]: description += format_leaderboard_user(top_user) + "\n" rank = get_rank(top_users[0]["gamma"]) if user: # Add separator if necessary if top_user_limit > top_count + 1: description += "...\n" # Show users with more gamma than the current user for above_user in above_users: description += format_leaderboard_user(above_user) + "\n" # Show the current user description += "**" + format_leaderboard_user(lb_user) + "**\n" # Show users with less gamma than the current user for below_user in below_users: description += format_leaderboard_user(below_user) + "\n" rank = get_rank(user["gamma"]) time_frame = format_leaderboard_timeframe(after_time, before_time) await msg.edit( content=i18n["leaderboard"]["embed_message"].format( user=get_username(user), time_str=time_str, duration=get_duration_str(start), ), embed=Embed( title=i18n["leaderboard"]["embed_title"].format( user=get_username(user), time_frame=time_frame ), description=description, color=Colour.from_rgb(*get_rgb_from_hex(rank["color"])), ), )
async def _until( self, ctx: SlashContext, goal: Optional[str] = None, username: str = "me", after: str = "1 week", before: Optional[str] = None, ) -> None: """Determine how long it will take the user to reach the given goal.""" start = datetime.now(tz=pytz.utc) after_time, before_time, time_str = parse_time_constraints( after, before) if not after_time: # We need a starting point for the calculations raise InvalidArgumentException("after", after) # Send a first message to show that the bot is responsive. # We will edit this message later with the actual content. msg = await ctx.send(i18n["until"]["getting_prediction"].format( user=get_initial_username(username, ctx), time_str=time_str, )) user = get_user(username, ctx, self.blossom_api) if goal is not None: try: # Check if the goal is a gamma value or rank name goal_gamma, goal_str = parse_goal_str(goal) except InvalidArgumentException: # The goal could be a username if not user: # If the user is the combined server, a target user doesn't make sense raise InvalidArgumentException("goal", goal) # Try to treat the goal as a user return await self._until_user_catch_up( ctx, msg, user, goal, start, after_time, before_time, time_str, ) elif user: # Take the next rank for the user next_rank = get_next_rank(user["gamma"]) if next_rank: goal_gamma, goal_str = parse_goal_str(next_rank["name"]) else: # If the user has reached the maximum rank, take the next 10,000 tier goal_gamma = ((user["gamma"] + 10_000) // 10_000) * 10_000 goal_str = f"{goal_gamma:,}" else: # You can't get the "next rank" of the whole server raise InvalidArgumentException("goal", "<empty>") user_gamma = get_user_gamma(user, self.blossom_api) await msg.edit( content=i18n["until"]["getting_prediction_to_goal"].format( user=get_username(user), goal=goal_str, time_str=time_str, )) description = await _get_progress_description( user, user_gamma, goal_gamma, goal_str, start, after_time, before_time, blossom_api=self.blossom_api, ) # Determine the color of the target rank color = get_rank(goal_gamma)["color"] await msg.edit( content=i18n["until"]["embed_message"].format( user=get_username(user), goal=goal_str, time_str=time_str, duration=get_duration_str(start), ), embed=Embed( title=i18n["until"]["embed_title"].format( user=get_username(user)), description=description, color=discord.Colour.from_rgb(*get_rgb_from_hex(color)), ), )
async def rate( self, ctx: SlashContext, usernames: str = "me", after: Optional[str] = None, before: Optional[str] = None, ) -> None: """Get the transcription rate of the user.""" start = datetime.now() after_time, before_time, time_str = parse_time_constraints( after, before) utc_offset = extract_utc_offset(ctx.author.display_name) # Give a quick response to let the user know we're working on it # We'll later edit this message with the actual content msg = await ctx.send(i18n["rate"]["getting_rate"].format( users=get_initial_username_list(usernames, ctx), time_str=time_str, )) users = get_user_list(usernames, ctx, self.blossom_api) if users: users.sort(key=lambda u: u["gamma"], reverse=True) colors = get_user_colors(users) max_rates = [] fig: plt.Figure = plt.figure() ax: plt.Axes = fig.gca() fig.subplots_adjust(bottom=0.2) ax.set_xlabel(i18n["rate"]["plot_xlabel"].format( timezone=utc_offset_to_str(utc_offset))) ax.set_ylabel(i18n["rate"]["plot_ylabel"]) for label in ax.get_xticklabels(): label.set_rotation(32) label.set_ha("right") ax.set_title(i18n["rate"]["plot_title"].format( users=get_usernames(users, 2, escape=False))) for index, user in enumerate(users or [None]): if users and len(users) > 1: await msg.edit(content=i18n["rate"]["getting_rate"].format( users=get_usernames(users), count=index + 1, total=len(users), time_str=time_str, )) user_data = self.get_all_rate_data(user, "day", after_time, before_time, utc_offset) max_rate = user_data["count"].max() max_rates.append(max_rate) max_rate_point = user_data[user_data["count"] == max_rate].iloc[0] color = colors[index] # Plot the graph ax.plot( "date", "count", data=user_data.reset_index(), color=color, ) # At a point for the max value ax.scatter( max_rate_point.name, max_rate_point.at["count"], color=color, s=4, ) # Label the max value ax.annotate( int(max_rate_point.at["count"]), xy=(max_rate_point.name, max_rate_point.at["count"]), color=color, ) if users: # A milestone at every 100 rate milestones = [ dict(threshold=i * 100, color=ranks[i + 2]["color"]) for i in range(1, 8) ] ax = add_milestone_lines(ax, milestones, 0, max(max_rates), 40) if users and len(users) > 1: ax.legend([get_username(user, escape=False) for user in users]) discord_file = create_file_from_figure(fig, "rate_plot.png") await msg.edit( content=i18n["rate"]["response_message"].format( usernames=get_usernames(users), time_str=time_str, duration=get_duration_str(start), ), file=discord_file, )
async def history( self, ctx: SlashContext, usernames: str = "me", after: Optional[str] = None, before: Optional[str] = None, ) -> None: """Get the transcription history of the user.""" start = datetime.now() after_time, before_time, time_str = parse_time_constraints( after, before) utc_offset = extract_utc_offset(ctx.author.display_name) # Give a quick response to let the user know we're working on it # We'll later edit this message with the actual content msg = await ctx.send(i18n["history"]["getting_history"].format( users=get_initial_username_list(usernames, ctx), time_str=time_str, )) users = get_user_list(usernames, ctx, self.blossom_api) if users: users.sort(key=lambda u: u["gamma"], reverse=True) colors = get_user_colors(users) min_gammas = [] max_gammas = [] fig: plt.Figure = plt.figure() ax: plt.Axes = fig.gca() fig.subplots_adjust(bottom=0.2) ax.set_xlabel(i18n["history"]["plot_xlabel"].format( timezone=utc_offset_to_str(utc_offset))) ax.set_ylabel(i18n["history"]["plot_ylabel"]) for label in ax.get_xticklabels(): label.set_rotation(32) label.set_ha("right") ax.set_title(i18n["history"]["plot_title"].format( users=get_usernames(users, 2, escape=False))) for index, user in enumerate(users or [None]): if users and len(users) > 1: await msg.edit( content=i18n["history"]["getting_history_progress"].format( users=get_usernames(users), time_str=time_str, count=index + 1, total=len(users), )) history_data = self.get_user_history(user, after_time, before_time, utc_offset) color = colors[index] first_point = history_data.iloc[0] last_point = history_data.iloc[-1] min_gammas.append(first_point.at["gamma"]) max_gammas.append(last_point.at["gamma"]) # Plot the graph ax.plot( "date", "gamma", data=history_data.reset_index(), color=color, ) # At a point for the last value ax.scatter( last_point.name, last_point.at["gamma"], color=color, s=4, ) # Label the last value ax.annotate( int(last_point.at["gamma"]), xy=(last_point.name, last_point.at["gamma"]), color=color, ) if users: # Show milestone lines min_value, max_value = min(min_gammas), max(max_gammas) delta = (max_value - min_value) * 0.4 ax = add_milestone_lines(ax, ranks, min_value, max_value, delta) if users and len(users) > 1: ax.legend([get_username(user, escape=False) for user in users]) discord_file = create_file_from_figure(fig, "history_plot.png") await msg.edit( content=i18n["history"]["response_message"].format( users=get_usernames(users), time_str=time_str, duration=get_duration_str(start), ), file=discord_file, )