aboutsummaryrefslogtreecommitdiffstats
path: root/bot/seasons
diff options
context:
space:
mode:
Diffstat (limited to 'bot/seasons')
-rw-r--r--bot/seasons/evergreen/trivia_quiz.py234
-rw-r--r--bot/seasons/halloween/hacktober-issue-finder.py107
-rw-r--r--bot/seasons/pride/drag_queen_name.py33
-rw-r--r--bot/seasons/pride/pride_facts.py106
4 files changed, 480 insertions, 0 deletions
diff --git a/bot/seasons/evergreen/trivia_quiz.py b/bot/seasons/evergreen/trivia_quiz.py
new file mode 100644
index 00000000..798523e6
--- /dev/null
+++ b/bot/seasons/evergreen/trivia_quiz.py
@@ -0,0 +1,234 @@
+import asyncio
+import json
+import logging
+import random
+from pathlib import Path
+
+import discord
+from discord.ext import commands
+from fuzzywuzzy import fuzz
+
+from bot.constants import Roles
+
+
+logger = logging.getLogger(__name__)
+
+
+ANNOYED_EXPRESSIONS = ["-_-", "-.-"]
+
+WRONG_ANS_RESPONSE = [
+ "No one gave the correct answer",
+ "Better luck next time"
+]
+
+
+class TriviaQuiz(commands.Cog):
+ """A cog for all quiz commands."""
+
+ def __init__(self, bot: commands.Bot) -> None:
+ self.bot = bot
+ self.questions = self.load_questions()
+ self.game_status = {}
+ self.game_owners = {}
+ self.question_limit = 4
+ self.player_dict = {}
+ self.categories = {
+ "general": "Test your general knowledge"
+ # "retro": "Questions related to retro gaming."
+ }
+
+ @staticmethod
+ def load_questions() -> dict:
+ """Load the questions from json file."""
+ p = Path("bot", "resources", "evergreen", "trivia_quiz.json")
+ with p.open() as json_data:
+ questions = json.load(json_data)
+ return questions
+
+ @commands.command(name="quiz", aliases=["trivia"])
+ async def quiz_game(self, ctx: commands.Context, category: str = "general") -> None:
+ """
+ Start/Stop a quiz!
+
+ arguments:
+ option:
+ - start : to start a quiz in a channel
+ - stop : stop the quiz running in that channel.
+
+ Questions for the quiz can be selected from the following categories:
+ - general : Test your general knowledge. (default)
+ (we wil be adding more later)
+ """
+ category = category.lower()
+
+ if ctx.channel.id not in self.game_status:
+ self.game_status[ctx.channel.id] = False
+ self.player_dict[ctx.channel.id] = {}
+
+ if not self.game_status[ctx.channel.id]:
+ self.game_owners[ctx.channel.id] = ctx.author
+ self.game_status[ctx.channel.id] = True
+ start_embed = discord.Embed(colour=discord.Colour.red())
+ start_embed.title = "Quiz game Starting!!"
+ start_embed.description = "Each game consists of 5 questions.\n"
+ start_embed.description += "**Rules :**\nNo cheating and have fun!"
+ start_embed.set_footer(
+ text="Points for a question reduces by 25 after 10s or after a hint. Total time is 30s per question"
+ )
+ await ctx.send(embed=start_embed) # send an embed with the rules
+ await asyncio.sleep(1)
+
+ else:
+ if (
+ ctx.author == self.game_owners[ctx.channel.id]
+ or Roles.moderator in [role.id for role in ctx.author.roles]
+ ):
+ await ctx.send("Quiz is no longer running.")
+ await self.declare_winner(ctx.channel, self.player_dict[ctx.channel.id])
+ self.game_status[ctx.channel.id] = False
+ del self.game_owners[ctx.channel.id]
+ else:
+ await ctx.send(f"{ctx.author.mention}, you are not authorised to stop this game :ghost: !")
+
+ if category not in self.categories:
+ embed = self.category_embed
+ await ctx.send(embed=embed)
+ return
+ topic = self.questions[category]
+
+ unanswered = 0
+ done_question = []
+ hint_no = 0
+ answer = None
+ while self.game_status[ctx.channel.id]:
+ if len(done_question) > self.question_limit and hint_no == 0:
+ await ctx.send("The round ends here.")
+ await self.declare_winner(ctx.channel, self.player_dict[ctx.channel.id])
+ break
+ if unanswered > 3:
+ await ctx.send("Game stopped due to inactivity.")
+ await self.declare_winner(ctx.channel, self.player_dict[ctx.channel.id])
+ break
+ if hint_no == 0:
+ while True:
+ question_dict = random.choice(topic)
+ if question_dict["id"] not in done_question:
+ done_question.append(question_dict["id"])
+ break
+ q = question_dict["question"]
+ answer = question_dict["answer"]
+
+ embed = discord.Embed(colour=discord.Colour.gold())
+ embed.title = f"Question #{len(done_question)}"
+ embed.description = q
+ await ctx.send(embed=embed)
+
+ def check(m: discord.Message) -> bool:
+ ratio = fuzz.ratio(answer.lower(), m.content.lower())
+ return ratio > 85 and m.channel == ctx.channel
+ try:
+ msg = await self.bot.wait_for('message', check=check, timeout=10)
+ except asyncio.TimeoutError:
+ if self.game_status[ctx.channel.id] is False:
+ break
+ if hint_no < 2:
+ hint_no += 1
+ if "hints" in question_dict:
+ hints = question_dict["hints"]
+ await ctx.send(f"**Hint #{hint_no+1}\n**{hints[hint_no]}")
+ else:
+ await ctx.send(f"Cmon guys, {30-hint_no*10}s left!")
+
+ else:
+ response = random.choice(WRONG_ANS_RESPONSE)
+ expression = random.choice(ANNOYED_EXPRESSIONS)
+ await ctx.send(f"{response} {expression}")
+ await self.send_answer(ctx.channel, question_dict)
+ await asyncio.sleep(1)
+ hint_no = 0
+ unanswered += 1
+ await self.send_score(ctx.channel, self.player_dict[ctx.channel.id])
+ await asyncio.sleep(2)
+
+ else:
+ points = 100 - 25*hint_no
+ if msg.author in self.player_dict[ctx.channel.id]:
+ self.player_dict[ctx.channel.id][msg.author] += points
+ else:
+ self.player_dict[ctx.channel.id][msg.author] = points
+ hint_no = 0
+ unanswered = 0
+ await ctx.send(f"{msg.author.mention} got the correct answer :tada: {points} points for ya.")
+ await self.send_answer(ctx.channel, question_dict)
+ await self.send_score(ctx.channel, self.player_dict[ctx.channel.id])
+ await asyncio.sleep(2)
+
+ @staticmethod
+ async def send_score(channel: discord.TextChannel, player_data: dict) -> None:
+ """A function which sends the score."""
+ embed = discord.Embed(colour=discord.Colour.blue())
+ embed.title = "Score Board"
+ embed.description = ""
+ for k, v in player_data.items():
+ embed.description += f"{k} : {v}\n"
+ await channel.send(embed=embed)
+
+ @staticmethod
+ async def declare_winner(channel: discord.TextChannel, player_data: dict) -> None:
+ """Announce the winner of the quiz in the game channel."""
+ if player_data:
+ highest_points = max(list(player_data.values()))
+ no_of_winners = list(player_data.values()).count(highest_points)
+
+ # Check if more than 1 player has highest points.
+ if no_of_winners > 1:
+ word = "You guys"
+ winners = []
+ points_copy = list(player_data.values()).copy()
+ for _ in range(no_of_winners):
+ index = points_copy.index(highest_points)
+ winners.append(list(player_data.keys())[index])
+ points_copy[index] = 0
+ winners_mention = None
+ for winner in winners:
+ winners_mention += f"{winner.mention} "
+
+ else:
+ word = "You"
+ author_index = list(player_data.values()).index(highest_points)
+ winner = list(player_data.keys())[author_index]
+ winners_mention = winner.mention
+ await channel.send(
+ f"Congratz {winners_mention} :tada: "
+ f"{word} have won this quiz game with a grand total of {highest_points} points!!"
+ )
+
+ @property
+ def category_embed(self) -> discord.Embed:
+ """Build an embed showing all available trivia categories."""
+ embed = discord.Embed(colour=discord.Colour.blue())
+ embed.title = "The available question categories are:"
+ embed.description = ""
+ for cat, description in self.categories.items():
+ embed.description += f"**- {cat.capitalize()}**\n{description.capitalize()}\n"
+ embed.set_footer(text="If not category is chosen, then a random one will be selected.")
+ return embed
+
+ @staticmethod
+ async def send_answer(channel: discord.TextChannel, question_dict: dict) -> None:
+ """Send the correct answer of a question to the game channel."""
+ answer = question_dict["answer"]
+ info = question_dict["info"]
+ embed = discord.Embed(color=discord.Colour.red())
+ embed.title = f"The correct answer is **{answer}**\n"
+ embed.description = ""
+ if info != "":
+ embed.description += f"**Information**\n{info}\n\n"
+ embed.description += "Lets move to the next question.\nRemaining questions: "
+ await channel.send(embed=embed)
+
+
+def setup(bot: commands.Bot) -> None:
+ """Loading the cog."""
+ bot.add_cog(TriviaQuiz(bot))
+ logger.debug("TriviaQuiz 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..2f1d6ac7
--- /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"]))
+ 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/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!")