aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/events/hacktoberfest/hacktober_issue_finder.py
blob: dbf8f7479718751365e6890d3c593da18b539435 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import random
from datetime import UTC, datetime

import discord
from discord.ext import commands
from pydis_core.utils.logging import get_logger

from bot.bot import Bot
from bot.constants import Month, Tokens
from bot.utils.decorators import in_month

log = get_logger(__name__)

URL = "https://api.github.com/search/issues?per_page=100&q=is:issue+label:hacktoberfest+language:python+state:open"

REQUEST_HEADERS = {
    "User-Agent": "Python Discord Hacktoberbot",
    "Accept": "application / vnd.github.v3 + json"
}
if GITHUB_TOKEN := Tokens.github.get_secret_value():
    REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}"


class HacktoberIssues(commands.Cog):
    """Find a random hacktober python issue on GitHub."""

    def __init__(self, bot: Bot):
        self.bot = bot
        self.cache_normal = None
        self.cache_timer_normal = datetime(1, 1, 1, tzinfo=UTC)
        self.cache_beginner = None
        self.cache_timer_beginner = datetime(1, 1, 1, tzinfo=UTC)

    @in_month(Month.OCTOBER)
    @commands.command()
    async def hacktoberissues(self, ctx: commands.Context, option: str = "") -> None:
        """
        Get a random python hacktober issue from Github.

        If the command is run with beginner (`.hacktoberissues beginner`):
        It will also narrow it down to the "first good issue" label.
        """
        async with ctx.typing():
            issues = await self.get_issues(ctx, option)
            if issues is None:
                return
            issue = random.choice(issues["items"])
            embed = self.format_embed(issue)
        await ctx.send(embed=embed)

    async def get_issues(self, ctx: commands.Context, option: str) -> dict | None:
        """Get a list of the python issues with the label 'hacktoberfest' from the Github api."""
        if option == "beginner":
            if (ctx.message.created_at.replace(tzinfo=None) - self.cache_timer_beginner).seconds <= 60:
                log.debug("using cache")
                return self.cache_beginner
        elif (ctx.message.created_at.replace(tzinfo=None) - self.cache_timer_normal).seconds <= 60:
            log.debug("using cache")
            return self.cache_normal

        if option == "beginner":
            url = URL + '+label:"good first issue"'
            if self.cache_beginner is not None:
                page = random.randint(1, min(1000, self.cache_beginner["total_count"]) // 100)
                url += f"&page={page}"
        else:
            url = URL
            if self.cache_normal is not None:
                page = random.randint(1, min(1000, self.cache_normal["total_count"]) // 100)
                url += f"&page={page}"

        log.debug(f"making api request to url: {url}")
        async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as response:
            if response.status != 200:
                log.error(f"expected 200 status (got {response.status}) by the GitHub api.")
                await ctx.send(
                    f"ERROR: expected 200 status (got {response.status}) by the GitHub api.\n"
                    f"{await response.text()}"
                )
                return None
            data = await response.json()

            if len(data["items"]) == 0:
                log.error(f"no issues returned by GitHub API, with url: {response.url}")
                await ctx.send(f"ERROR: no issues returned by GitHub API, with url: {response.url}")
                return None

            if option == "beginner":
                self.cache_beginner = data
                self.cache_timer_beginner = ctx.message.created_at.replace(tzinfo=None)
            else:
                self.cache_normal = data
                self.cache_timer_normal = ctx.message.created_at.replace(tzinfo=None)

            return data

    @staticmethod
    def format_embed(issue: dict) -> discord.Embed:
        """Format the issue data into a embed."""
        title = issue["title"]
        issue_url = issue["url"].replace("api.", "").replace("/repos/", "/")
        # Issues can have empty bodies, resulting in the value being a literal `null` (parsed as `None`).
        # For this reason, we can't use the default arg of `dict.get`, and so instead use `or` logic.
        body = issue.get("body") or ""
        labels = [label["name"] for label in issue["labels"]]

        embed = discord.Embed(title=title)
        embed.description = body[:500] + "..." if len(body) > 500 else body
        embed.add_field(name="labels", value="\n".join(labels))
        embed.url = issue_url
        embed.set_footer(text=issue_url)

        return embed


async def setup(bot: Bot) -> None:
    """Load the HacktoberIssue finder."""
    await bot.add_cog(HacktoberIssues(bot))