diff options
Diffstat (limited to 'bot/seasons')
| -rw-r--r-- | bot/seasons/evergreen/trivia_quiz.py | 234 | ||||
| -rw-r--r-- | bot/seasons/halloween/hacktober-issue-finder.py | 107 | ||||
| -rw-r--r-- | bot/seasons/pride/drag_queen_name.py | 33 | ||||
| -rw-r--r-- | bot/seasons/pride/pride_facts.py | 106 |
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!") |