Example #1
0
async def send_rules_message(
    msg: SlashMessage,
    rules: List[Rule],
    subreddit: str,
    start_time: datetime,
    localization_key: str,
) -> None:
    """Send an embed containing the rules to the user."""
    embed = Embed(
        title=i18n[localization_key]["embed_title"].format(subreddit))

    for rule in rules:
        # The value field is not allowed to be a blank string
        # So we just repeat the name of the rule if it is not provided
        embed.add_field(
            name=rule.short_name,
            value=rule.description or rule.short_name,
            inline=False,
        )

    await msg.edit(
        content=i18n["rules"]["embed_message"].format(
            get_duration_str(start_time)),
        embed=embed,
    )
Example #2
0
    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,
        )
Example #3
0
    async def _find(self, ctx: SlashContext, reddit_url: str) -> None:
        """Find the post with the given URL."""
        start = datetime.now()

        # 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["find"]["looking_for_posts"].format(url=reddit_url))

        find_response = self.blossom_api.get("find", params={"url": reddit_url})
        if not find_response.ok:
            await msg.edit(content=i18n["find"]["not_found"].format(url=reddit_url))
            return
        data = find_response.json()

        await msg.edit(
            content=i18n["find"]["embed_message"].format(
                duration=get_duration_str(start)
            ),
            embed=to_embed(data),
        )
Example #4
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"])),
            ),
        )
Example #5
0
    async def _search_from_cache(
        self,
        msg: SlashMessage,
        start: datetime,
        cache_item: SearchCacheItem,
        page_mod: int,
    ) -> None:
        """Execute the search with the given cache."""
        # Clear previous control emojis
        await clear_reactions(msg)

        discord_page = cache_item["cur_page"] + page_mod
        query = cache_item["query"]
        user = cache_item["user"]
        user_id = user["id"] if user else None
        after_time = cache_item["after_time"]
        before_time = cache_item["before_time"]
        time_str = cache_item["time_str"]

        from_str = after_time.isoformat() if after_time else None
        until_str = before_time.isoformat() if before_time else None

        request_page = (discord_page *
                        self.discord_page_size) // self.request_page_size

        if (not cache_item["response_data"]
                or request_page != cache_item["request_page"]):
            # A new request has to be made
            data = {
                "text__icontains": cache_item["query"],
                "author": user_id,
                "create_time__gte": from_str,
                "create_time__lte": until_str,
                "url__isnull": False,
                "ordering": "-create_time",
                "page_size": self.request_page_size,
                "page": request_page + 1,
            }
            response = self.blossom_api.get(path="transcription", params=data)
            if response.status_code != 200:
                raise BlossomException(response)
            response_data = response.json()
        else:
            response_data = cache_item["response_data"]

        if response_data["count"] == 0:
            await msg.edit(content=i18n["search"]["no_results"].format(
                query=query,
                user=get_username(user),
                time_str=time_str,
                duration_str=get_duration_str(start),
            ))
            return

        # Only cache the result if the user can change pages
        if response_data["count"] > self.discord_page_size:
            # Update the cache
            self.cache.set(
                msg.id,
                {
                    "query": query,
                    "user": cache_item["user"],
                    "after_time": after_time,
                    "before_time": before_time,
                    "time_str": time_str,
                    "cur_page": discord_page,
                    "discord_user_id": cache_item["discord_user_id"],
                    "response_data": response_data,
                    "request_page": request_page,
                },
            )

        # Calculate the offset within the response
        # The requested pages are larger than the pages displayed on Discord
        request_offset = request_page * self.request_page_size
        discord_offset = discord_page * self.discord_page_size
        result_offset = discord_offset - request_offset
        page_results: List[Dict[
            str, Any]] = response_data["results"][result_offset:result_offset +
                                                  self.discord_page_size]
        description = ""

        for i, res in enumerate(page_results):
            description += create_result_description(res,
                                                     discord_offset + i + 1,
                                                     query)

        total_discord_pages = math.ceil(response_data["count"] /
                                        self.discord_page_size)

        await msg.edit(
            content=i18n["search"]["embed_message"].format(
                query=query,
                user=get_username(user),
                time_str=time_str,
                duration_str=get_duration_str(start),
            ),
            embed=Embed(
                title=i18n["search"]["embed_title"].format(
                    query=query, user=get_username(user)),
                description=description,
            ).set_footer(text=i18n["search"]["embed_footer"].format(
                cur_page=discord_page + 1,
                total_pages=total_discord_pages,
                total_results=response_data["count"],
            ), ),
        )

        emoji_controls = []

        # Determine which controls are appropriate
        if discord_page > 0:
            emoji_controls.append(first_page_emoji)
            emoji_controls.append(previous_page_emoji)
        if discord_page < total_discord_pages - 1:
            emoji_controls.append(next_page_emoji)
            emoji_controls.append(last_page_emoji)

        # Add control emojis to message
        await asyncio.gather(
            *[msg.add_reaction(emoji) for emoji in emoji_controls])
Example #6
0
    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)),
            ),
        )
Example #7
0
    async def _until_user_catch_up(
        self,
        ctx: SlashContext,
        msg: SlashMessage,
        user: BlossomUser,
        target_username: str,
        start: datetime,
        after_time: datetime,
        before_time: Optional[datetime],
        time_str: str,
    ) -> None:
        """Determine how long it will take the user to catch up with the target user."""
        # Try to find the target user
        try:
            target = get_user(target_username, ctx, self.blossom_api)
        except UserNotFound:
            # This doesn't mean the username is wrong
            # They could have also mistyped a rank
            # So we change the error message to something else
            raise InvalidArgumentException("goal", target_username)

        if not target:
            # Having the combined server as target doesn't make sense
            # Because it includes the current user, they could never reach it
            raise InvalidArgumentException("goal", target_username)

        if user["gamma"] > target["gamma"]:
            # Swap user and target, the target has to have more gamma
            # Otherwise the goal would have already been reached
            user, target = target, user

        user_progress = await _get_user_progress(user,
                                                 after_time,
                                                 before_time,
                                                 blossom_api=self.blossom_api)
        target_progress = await _get_user_progress(
            target, after_time, before_time, blossom_api=self.blossom_api)

        time_frame = (before_time or start) - after_time

        if user_progress <= target_progress:
            description = i18n["until"]["embed_description_user_never"].format(
                user=get_username(user),
                user_gamma=user["gamma"],
                user_progress=user_progress,
                target=get_username(target),
                target_gamma=target["gamma"],
                target_progress=target_progress,
                time_frame=get_timedelta_str(time_frame),
            )
        else:
            # Calculate time needed
            seconds_needed = (target["gamma"] - user["gamma"]) / (
                (user_progress - target_progress) / time_frame.total_seconds())
            relative_time = timedelta(seconds=seconds_needed)
            absolute_time = start + relative_time

            intersection_gamma = user["gamma"] + math.ceil(
                (user_progress / time_frame.total_seconds()) *
                relative_time.total_seconds())

            description = i18n["until"][
                "embed_description_user_prediction"].format(
                    user=get_username(user),
                    user_gamma=user["gamma"],
                    user_progress=user_progress,
                    target=get_username(target),
                    target_gamma=target["gamma"],
                    target_progress=target_progress,
                    intersection_gamma=intersection_gamma,
                    time_frame=get_timedelta_str(time_frame),
                    relative_time=get_timedelta_str(relative_time),
                    absolute_time=get_discord_time_str(absolute_time),
                )

        color = get_rank(target["gamma"])["color"]

        await msg.edit(
            content=i18n["until"]["embed_message"].format(
                user=get_username(user),
                goal=get_username(target),
                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)),
            ),
        )
Example #8
0
    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,
        )
Example #9
0
    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,
        )
Example #10
0
    async def _partner(self,
                       ctx: SlashContext,
                       subreddit: Optional[str] = None) -> None:
        """Get the list of all our partner subreddits."""
        start = datetime.now()

        if subreddit is None:
            msg = await ctx.send(i18n["partner"]["getting_partner_list"])
        else:
            msg = await ctx.send(
                i18n["partner"]["getting_partner_status"].format(
                    subreddit=subreddit))

        partners = await self._get_partner_list()

        if subreddit is None:
            partner_str = join_items_with_and(partners)
            await msg.edit(
                content=i18n["partner"]["embed_partner_list_message"].format(
                    duration=get_duration_str(start)),
                embed=Embed(
                    title=i18n["partner"]["embed_partner_list_title"],
                    description=i18n["partner"]
                    ["embed_partner_list_description"].format(
                        count=len(partners), partner_list=partner_str),
                ),
            )
        else:
            sub = await self.reddit_api.subreddit(subreddit)
            is_private = False

            try:
                await sub.load()
            except Redirect:
                # The subreddit does not exist
                await msg.edit(content=i18n["partner"]["sub_not_found"].format(
                    subreddit=subreddit))
                return
            except NotFound:
                # A character in the sub name is not allowed
                await msg.edit(content=i18n["partner"]["sub_not_found"].format(
                    subreddit=subreddit))
                return
            except Forbidden:
                # The subreddit is private
                is_private = True

            is_partner = subreddit.casefold() in [
                partner.casefold() for partner in partners
            ]
            message = i18n["partner"]["embed_partner_status_message"].format(
                subreddit=subreddit, duration=get_duration_str(start))

            status_message = (i18n["partner"]["status_yes_message"].format(
                subreddit=subreddit) if is_partner else
                              i18n["partner"]["status_no_message"].format(
                                  subreddit=subreddit))

            if is_private:
                status_message += i18n["partner"]["private_message"]
            else:
                status_message += "\n" + i18n["partner"][
                    "sub_description"].format(
                        description=sub.public_description)

            color = (Color.red() if not is_partner else
                     Color.orange() if is_private else Color.green())

            await msg.edit(
                content=message,
                embed=Embed(
                    title=i18n["partner"]["embed_partner_status_title"].format(
                        subreddit=subreddit),
                    description=i18n["partner"]
                    ["embed_partner_status_description"].format(
                        status=status_message),
                    color=color,
                ),
            )