aboutsummaryrefslogtreecommitdiffstats
path: root/bot/seasons
diff options
context:
space:
mode:
authorGravatar Rohan_Iceman <[email protected]>2019-11-28 18:36:04 +0530
committerGravatar GitHub <[email protected]>2019-11-28 18:36:04 +0530
commitadf81472c3509e380ef06b53d699bbcedcd04aaf (patch)
tree6c1e75b409343e76129917c686a32c7fe85475f1 /bot/seasons
parentGrammar fixes (diff)
parentAdd the new blinky server guild icon (#314) (diff)
Merge branch 'master' into quiz_fix
Diffstat (limited to 'bot/seasons')
-rw-r--r--bot/seasons/christmas/adventofcode.py14
-rw-r--r--bot/seasons/evergreen/__init__.py11
-rw-r--r--bot/seasons/evergreen/issues.py73
-rw-r--r--bot/seasons/halloween/hacktober-issue-finder.py107
-rw-r--r--bot/seasons/halloween/hacktoberstats.py2
-rw-r--r--bot/seasons/halloween/timeleft.py2
-rw-r--r--bot/seasons/pride/drag_queen_name.py33
-rw-r--r--bot/seasons/pride/pride_facts.py106
8 files changed, 315 insertions, 33 deletions
diff --git a/bot/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py
index 513c1020..007e4783 100644
--- a/bot/seasons/christmas/adventofcode.py
+++ b/bot/seasons/christmas/adventofcode.py
@@ -13,7 +13,7 @@ from bs4 import BeautifulSoup
from discord.ext import commands
from pytz import timezone
-from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Tokens
+from bot.constants import AdventOfCode as AocConfig, Channels, Colours, Emojis, Tokens, WHITELISTED_CHANNELS
from bot.decorators import override_in_channel
log = logging.getLogger(__name__)
@@ -24,6 +24,8 @@ AOC_SESSION_COOKIE = {"session": Tokens.aoc_session_cookie}
EST = timezone("EST")
COUNTDOWN_STEP = 60 * 5
+AOC_WHITELIST = WHITELISTED_CHANNELS + (Channels.advent_of_code,)
+
def is_in_advent() -> bool:
"""Utility function to check if we are between December 1st and December 25th."""
@@ -126,7 +128,7 @@ class AdventOfCode(commands.Cog):
self.status_task = asyncio.ensure_future(self.bot.loop.create_task(status_coro))
@commands.group(name="adventofcode", aliases=("aoc",), invoke_without_command=True)
- @override_in_channel()
+ @override_in_channel(AOC_WHITELIST)
async def adventofcode_group(self, ctx: commands.Context) -> None:
"""All of the Advent of Code commands."""
await ctx.send_help(ctx.command)
@@ -136,6 +138,7 @@ class AdventOfCode(commands.Cog):
aliases=("sub", "notifications", "notify", "notifs"),
brief="Notifications for new days"
)
+ @override_in_channel(AOC_WHITELIST)
async def aoc_subscribe(self, ctx: commands.Context) -> None:
"""Assign the role for notifications about new days being ready."""
role = ctx.guild.get_role(AocConfig.role_id)
@@ -150,6 +153,7 @@ class AdventOfCode(commands.Cog):
f"If you don't want them any more, run `{unsubscribe_command}` instead.")
@adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days")
+ @override_in_channel(AOC_WHITELIST)
async def aoc_unsubscribe(self, ctx: commands.Context) -> None:
"""Remove the role for notifications about new days being ready."""
role = ctx.guild.get_role(AocConfig.role_id)
@@ -161,6 +165,7 @@ class AdventOfCode(commands.Cog):
await ctx.send("Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.")
@adventofcode_group.command(name="countdown", aliases=("count", "c"), brief="Return time left until next day")
+ @override_in_channel(AOC_WHITELIST)
async def aoc_countdown(self, ctx: commands.Context) -> None:
"""Return time left until next day."""
if not is_in_advent():
@@ -178,11 +183,13 @@ class AdventOfCode(commands.Cog):
await ctx.send(f"There are {hours} hours and {minutes} minutes left until day {tomorrow.day}.")
@adventofcode_group.command(name="about", aliases=("ab", "info"), brief="Learn about Advent of Code")
+ @override_in_channel(AOC_WHITELIST)
async def about_aoc(self, ctx: commands.Context) -> None:
"""Respond with an explanation of all things Advent of Code."""
await ctx.send("", embed=self.cached_about_aoc)
@adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join PyDis' private AoC leaderboard")
+ @override_in_channel(AOC_WHITELIST)
async def join_leaderboard(self, ctx: commands.Context) -> None:
"""DM the user the information for joining the PyDis AoC private leaderboard."""
author = ctx.message.author
@@ -203,6 +210,7 @@ class AdventOfCode(commands.Cog):
aliases=("board", "lb"),
brief="Get a snapshot of the PyDis private AoC leaderboard",
)
+ @override_in_channel(AOC_WHITELIST)
async def aoc_leaderboard(self, ctx: commands.Context, number_of_people_to_display: int = 10) -> None:
"""
Pull the top number_of_people_to_display members from the PyDis leaderboard and post an embed.
@@ -244,6 +252,7 @@ class AdventOfCode(commands.Cog):
aliases=("dailystats", "ds"),
brief="Get daily statistics for the PyDis private leaderboard"
)
+ @override_in_channel(AOC_WHITELIST)
async def private_leaderboard_daily_stats(self, ctx: commands.Context) -> None:
"""
Respond with a table of the daily completion statistics for the PyDis private leaderboard.
@@ -287,6 +296,7 @@ class AdventOfCode(commands.Cog):
aliases=("globalboard", "gb"),
brief="Get a snapshot of the global AoC leaderboard",
)
+ @override_in_channel(AOC_WHITELIST)
async def global_leaderboard(self, ctx: commands.Context, number_of_people_to_display: int = 10) -> None:
"""
Pull the top number_of_people_to_display members from the global AoC leaderboard and post an embed.
diff --git a/bot/seasons/evergreen/__init__.py b/bot/seasons/evergreen/__init__.py
index b95f3528..c2746552 100644
--- a/bot/seasons/evergreen/__init__.py
+++ b/bot/seasons/evergreen/__init__.py
@@ -6,8 +6,11 @@ class Evergreen(SeasonBase):
bot_icon = "/logos/logo_seasonal/evergreen/logo_evergreen.png"
icon = (
- "/logos/logo_animated/heartbeat/heartbeat.gif",
- "/logos/logo_animated/spinner/spinner.gif",
- "/logos/logo_animated/tongues/tongues.gif",
- "/logos/logo_animated/winky/winky.gif",
+ "/logos/logo_animated/heartbeat/heartbeat_512.gif",
+ "/logos/logo_animated/spinner/spinner_512.gif",
+ "/logos/logo_animated/tongues/tongues_512.gif",
+ "/logos/logo_animated/winky/winky_512.gif",
+ "/logos/logo_animated/jumper/jumper_512.gif",
+ "/logos/logo_animated/apple/apple_512.gif",
+ "/logos/logo_animated/blinky/blinky_512.gif",
)
diff --git a/bot/seasons/evergreen/issues.py b/bot/seasons/evergreen/issues.py
index 438ab475..c7501a5d 100644
--- a/bot/seasons/evergreen/issues.py
+++ b/bot/seasons/evergreen/issues.py
@@ -3,10 +3,16 @@ import logging
import discord
from discord.ext import commands
-from bot.constants import Colours
+from bot.constants import Channels, Colours, Emojis, WHITELISTED_CHANNELS
from bot.decorators import override_in_channel
log = logging.getLogger(__name__)
+ISSUE_WHITELIST = WHITELISTED_CHANNELS + (Channels.seasonalbot_chat,)
+
+BAD_RESPONSE = {
+ 404: "Issue/pull request not located! Please enter a valid number!",
+ 403: "Rate limit has been hit! Please try again later!"
+}
class Issues(commands.Cog):
@@ -15,43 +21,58 @@ class Issues(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
- @commands.command(aliases=("issues",))
- @override_in_channel()
+ @commands.command(aliases=("pr",))
+ @override_in_channel(ISSUE_WHITELIST)
async def issue(
self, ctx: commands.Context, number: int, repository: str = "seasonalbot", user: str = "python-discord"
) -> None:
"""Command to retrieve issues from a GitHub repository."""
- api_url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}"
- failed_status = {
- 404: f"Issue #{number} doesn't exist in the repository {user}/{repository}.",
- 403: f"Rate limit exceeded. Please wait a while before trying again!"
- }
+ url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}"
+ merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge"
- async with self.bot.http_session.get(api_url) as r:
+ log.trace(f"Querying GH issues API: {url}")
+ async with self.bot.http_session.get(url) as r:
json_data = await r.json()
- response_code = r.status
-
- if response_code in failed_status:
- return await ctx.send(failed_status[response_code])
- repo_url = f"https://github.com/{user}/{repository}"
- issue_embed = discord.Embed(colour=Colours.bright_green)
- issue_embed.add_field(name="Repository", value=f"[{user}/{repository}]({repo_url})", inline=False)
- issue_embed.add_field(name="Issue Number", value=f"#{number}", inline=False)
- issue_embed.add_field(name="Status", value=json_data["state"].title())
- issue_embed.add_field(name="Link", value=json_data["html_url"], inline=False)
+ if r.status in BAD_RESPONSE:
+ log.warning(f"Received response {r.status} from: {url}")
+ return await ctx.send(f"[{str(r.status)}] {BAD_RESPONSE.get(r.status)}")
- description = json_data["body"]
- if len(description) > 1024:
- placeholder = " [...]"
- description = f"{description[:1024 - len(placeholder)]}{placeholder}"
+ # The initial API request is made to the issues API endpoint, which will return information
+ # if the issue or PR is present. However, the scope of information returned for PRs differs
+ # from issues: if the 'issues' key is present in the response then we can pull the data we
+ # need from the initial API call.
+ if "issues" in json_data.get("html_url"):
+ if json_data.get("state") == "open":
+ icon_url = Emojis.issue
+ else:
+ icon_url = Emojis.issue_closed
- issue_embed.add_field(name="Description", value=description, inline=False)
+ # If the 'issues' key is not contained in the API response and there is no error code, then
+ # we know that a PR has been requested and a call to the pulls API endpoint is necessary
+ # to get the desired information for the PR.
+ else:
+ log.trace(f"PR provided, querying GH pulls API for additional information: {merge_url}")
+ async with self.bot.http_session.get(merge_url) as m:
+ if json_data.get("state") == "open":
+ icon_url = Emojis.pull_request
+ # When the status is 204 this means that the state of the PR is merged
+ elif m.status == 204:
+ icon_url = Emojis.merge
+ else:
+ icon_url = Emojis.pull_request_closed
- await ctx.send(embed=issue_embed)
+ issue_url = json_data.get("html_url")
+ description_text = f"[{repository}] #{number} {json_data.get('title')}"
+ resp = discord.Embed(
+ colour=Colours.bright_green,
+ description=f"{icon_url} [{description_text}]({issue_url})"
+ )
+ resp.set_author(name="GitHub", url=issue_url)
+ await ctx.send(embed=resp)
def setup(bot: commands.Bot) -> None:
- """Github Issues Cog Load."""
+ """Cog Retrieves Issues From Github."""
bot.add_cog(Issues(bot))
log.info("Issues cog loaded")
diff --git a/bot/seasons/halloween/hacktober-issue-finder.py b/bot/seasons/halloween/hacktober-issue-finder.py
new file mode 100644
index 00000000..10732374
--- /dev/null
+++ b/bot/seasons/halloween/hacktober-issue-finder.py
@@ -0,0 +1,107 @@
+import datetime
+import logging
+import random
+from typing import Dict, Optional
+
+import aiohttp
+import discord
+from discord.ext import commands
+
+log = logging.getLogger(__name__)
+
+URL = "https://api.github.com/search/issues?per_page=100&q=is:issue+label:hacktoberfest+language:python+state:open"
+HEADERS = {"Accept": "application / vnd.github.v3 + json"}
+
+
+class HacktoberIssues(commands.Cog):
+ """Find a random hacktober python issue on GitHub."""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+ self.cache_normal = None
+ self.cache_timer_normal = datetime.datetime(1, 1, 1)
+ self.cache_beginner = None
+ self.cache_timer_beginner = datetime.datetime(1, 1, 1)
+
+ @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.
+ """
+ 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) -> Optional[Dict]:
+ """Get a list of the python issues with the label 'hacktoberfest' from the Github api."""
+ if option == "beginner":
+ if (ctx.message.created_at - self.cache_timer_beginner).seconds <= 60:
+ log.debug("using cache")
+ return self.cache_beginner
+ elif (ctx.message.created_at - self.cache_timer_normal).seconds <= 60:
+ log.debug("using cache")
+ return self.cache_normal
+
+ async with aiohttp.ClientSession() as session:
+ 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 session.get(url, headers=HEADERS) as response:
+ if response.status != 200:
+ log.error(f"expected 200 status (got {response.status}) from the GitHub api.")
+ await ctx.send(f"ERROR: expected 200 status (got {response.status}) from the GitHub api.")
+ await ctx.send(await response.text())
+ return None
+ data = await response.json()
+
+ if len(data["items"]) == 0:
+ log.error(f"no issues returned from GitHub api. with url: {response.url}")
+ await ctx.send(f"ERROR: no issues returned from GitHub api. with url: {response.url}")
+ return None
+
+ if option == "beginner":
+ self.cache_beginner = data
+ self.cache_timer_beginner = ctx.message.created_at
+ else:
+ self.cache_normal = data
+ self.cache_timer_normal = ctx.message.created_at
+
+ 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/", "/")
+ body = issue["body"]
+ labels = [label["name"] for label in issue["labels"]]
+
+ embed = discord.Embed(title=title)
+ embed.description = body
+ embed.add_field(name="labels", value="\n".join(labels))
+ embed.url = issue_url
+ embed.set_footer(text=issue_url)
+
+ return embed
+
+
+def setup(bot: commands.Bot) -> None:
+ """Hacktober issue finder Cog Load."""
+ bot.add_cog(HacktoberIssues(bot))
+ log.info("hacktober-issue-finder cog loaded")
diff --git a/bot/seasons/halloween/hacktoberstats.py b/bot/seasons/halloween/hacktoberstats.py
index 9ad44e3f..ab8d865c 100644
--- a/bot/seasons/halloween/hacktoberstats.py
+++ b/bot/seasons/halloween/hacktoberstats.py
@@ -58,6 +58,7 @@ class HacktoberStats(commands.Cog):
await self.get_stats(ctx, github_username)
@hacktoberstats_group.command(name="link")
+ @override_in_channel(HACKTOBER_WHITELIST)
async def link_user(self, ctx: commands.Context, github_username: str = None) -> None:
"""
Link the invoking user's Github github_username to their Discord ID.
@@ -91,6 +92,7 @@ class HacktoberStats(commands.Cog):
await ctx.send(f"{author_mention}, a GitHub username is required to link your account")
@hacktoberstats_group.command(name="unlink")
+ @override_in_channel(HACKTOBER_WHITELIST)
async def unlink_user(self, ctx: commands.Context) -> None:
"""Remove the invoking user's account link from the log."""
author_id, author_mention = HacktoberStats._author_mention_from_context(ctx)
diff --git a/bot/seasons/halloween/timeleft.py b/bot/seasons/halloween/timeleft.py
index 77767baa..8cb3f4f6 100644
--- a/bot/seasons/halloween/timeleft.py
+++ b/bot/seasons/halloween/timeleft.py
@@ -25,7 +25,7 @@ class TimeLeft(commands.Cog):
year = now.year
if now.month > 10:
year += 1
- end = datetime(year, 10, 31, 11, 59, 59)
+ end = datetime(year, 11, 1, 11, 59, 59)
start = datetime(year, 10, 1)
return now, end, start
diff --git a/bot/seasons/pride/drag_queen_name.py b/bot/seasons/pride/drag_queen_name.py
new file mode 100644
index 00000000..43813fbd
--- /dev/null
+++ b/bot/seasons/pride/drag_queen_name.py
@@ -0,0 +1,33 @@
+import json
+import logging
+import random
+from pathlib import Path
+
+from discord.ext import commands
+
+log = logging.getLogger(__name__)
+
+
+class DragNames(commands.Cog):
+ """Gives a random drag queen name!"""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+ self.names = self.load_names()
+
+ @staticmethod
+ def load_names() -> list:
+ """Loads a list of drag queen names."""
+ with open(Path("bot/resources/pride/drag_queen_names.json"), "r", encoding="utf-8") as f:
+ return json.load(f)
+
+ @commands.command(name="dragname", aliases=["dragqueenname", "queenme"])
+ async def dragname(self, ctx: commands.Context) -> None:
+ """Sends a message with a drag queen name."""
+ await ctx.send(random.choice(self.names))
+
+
+def setup(bot: commands.Bot) -> None:
+ """Cog loader for drag queen name generator."""
+ bot.add_cog(DragNames(bot))
+ log.info("Drag queen name generator cog loaded!")
diff --git a/bot/seasons/pride/pride_facts.py b/bot/seasons/pride/pride_facts.py
new file mode 100644
index 00000000..b705bfb4
--- /dev/null
+++ b/bot/seasons/pride/pride_facts.py
@@ -0,0 +1,106 @@
+import asyncio
+import json
+import logging
+import random
+from datetime import datetime
+from pathlib import Path
+from typing import Union
+
+import dateutil.parser
+import discord
+from discord.ext import commands
+
+from bot.constants import Channels
+from bot.constants import Colours
+
+log = logging.getLogger(__name__)
+
+Sendable = Union[commands.Context, discord.TextChannel]
+
+
+class PrideFacts(commands.Cog):
+ """Provides a new fact every day during the Pride season!"""
+
+ def __init__(self, bot: commands.Bot):
+ self.bot = bot
+ self.facts = self.load_facts()
+
+ @staticmethod
+ def load_facts() -> dict:
+ """Loads a dictionary of years mapping to lists of facts."""
+ with open(Path("bot/resources/pride/facts.json"), "r", encoding="utf-8") as f:
+ return json.load(f)
+
+ async def send_pride_fact_daily(self) -> None:
+ """Background task to post the daily pride fact every day."""
+ channel = self.bot.get_channel(Channels.seasonalbot_chat)
+ while True:
+ await self.send_select_fact(channel, datetime.utcnow())
+ await asyncio.sleep(24 * 60 * 60)
+
+ async def send_random_fact(self, ctx: commands.Context) -> None:
+ """Provides a fact from any previous day, or today."""
+ now = datetime.utcnow()
+ previous_years_facts = (self.facts[x] for x in self.facts.keys() if int(x) < now.year)
+ current_year_facts = self.facts.get(str(now.year), [])[:now.day]
+ previous_facts = current_year_facts + [x for y in previous_years_facts for x in y]
+ try:
+ await ctx.send(embed=self.make_embed(random.choice(previous_facts)))
+ except IndexError:
+ await ctx.send("No facts available")
+
+ async def send_select_fact(self, target: Sendable, _date: Union[str, datetime]) -> None:
+ """Provides the fact for the specified day, if the day is today, or is in the past."""
+ now = datetime.utcnow()
+ if isinstance(_date, str):
+ try:
+ date = dateutil.parser.parse(_date, dayfirst=False, yearfirst=False, fuzzy=True)
+ except (ValueError, OverflowError) as err:
+ await target.send(f"Error parsing date: {err}")
+ return
+ else:
+ date = _date
+ if date.year < now.year or (date.year == now.year and date.day <= now.day):
+ try:
+ await target.send(embed=self.make_embed(self.facts[str(date.year)][date.day - 1]))
+ except KeyError:
+ await target.send(f"The year {date.year} is not yet supported")
+ return
+ except IndexError:
+ await target.send(f"Day {date.day} of {date.year} is not yet support")
+ return
+ else:
+ await target.send("The fact for the selected day is not yet available.")
+
+ @commands.command(name="pridefact", aliases=["pridefacts"])
+ async def pridefact(self, ctx: commands.Context) -> None:
+ """
+ Sends a message with a pride fact of the day.
+
+ If "random" is given as an argument, a random previous fact will be provided.
+
+ If a date is given as an argument, and the date is in the past, the fact from that day
+ will be provided.
+ """
+ message_body = ctx.message.content[len(ctx.invoked_with) + 2:]
+ if message_body == "":
+ await self.send_select_fact(ctx, datetime.utcnow())
+ elif message_body.lower().startswith("rand"):
+ await self.send_random_fact(ctx)
+ else:
+ await self.send_select_fact(ctx, message_body)
+
+ def make_embed(self, fact: str) -> discord.Embed:
+ """Makes a nice embed for the fact to be sent."""
+ return discord.Embed(
+ colour=Colours.pink,
+ title="Pride Fact!",
+ description=fact
+ )
+
+
+def setup(bot: commands.Bot) -> None:
+ """Cog loader for pride facts."""
+ bot.loop.create_task(PrideFacts(bot).send_pride_fact_daily())
+ bot.add_cog(PrideFacts(bot))
+ log.info("Pride facts cog loaded!")