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