diff options
-rw-r--r-- | bot/seasons/christmas/adventofcode.py | 2 | ||||
-rw-r--r-- | bot/seasons/easter/bunny_name_generator.py | 4 | ||||
-rw-r--r-- | bot/seasons/easter/egghead_quiz.py | 2 | ||||
-rw-r--r-- | bot/seasons/evergreen/8bitify.py | 6 | ||||
-rw-r--r-- | bot/seasons/evergreen/minesweeper.py | 10 | ||||
-rw-r--r-- | bot/seasons/evergreen/showprojects.py | 2 | ||||
-rw-r--r-- | bot/seasons/evergreen/snakes/utils.py | 62 | ||||
-rw-r--r-- | bot/seasons/halloween/candy_collection.py | 11 | ||||
-rw-r--r-- | bot/seasons/halloween/hacktoberstats.py | 49 | ||||
-rw-r--r-- | bot/seasons/halloween/monstersurvey.py | 2 | ||||
-rw-r--r-- | bot/seasons/halloween/spookyrating.py | 2 | ||||
-rw-r--r-- | tox.ini | 3 |
12 files changed, 78 insertions, 77 deletions
diff --git a/bot/seasons/christmas/adventofcode.py b/bot/seasons/christmas/adventofcode.py index 5938ccbe..6609387e 100644 --- a/bot/seasons/christmas/adventofcode.py +++ b/bot/seasons/christmas/adventofcode.py @@ -359,7 +359,7 @@ class AdventOfCode(commands.Cog): ) async def _check_n_entries(self, ctx: commands.Context, number_of_people_to_display: int) -> int: - """Check for n > max_entries and n <= 0""" + """Check for n > max_entries and n <= 0.""" max_entries = AocConfig.leaderboard_max_displayed_members author = ctx.message.author if not 0 <= number_of_people_to_display <= max_entries: diff --git a/bot/seasons/easter/bunny_name_generator.py b/bot/seasons/easter/bunny_name_generator.py index b068ac2c..22957b7f 100644 --- a/bot/seasons/easter/bunny_name_generator.py +++ b/bot/seasons/easter/bunny_name_generator.py @@ -29,8 +29,8 @@ class BunnyNameGenerator(commands.Cog): """ Finds vowels in the user's display name. - If the Discord name contains a vowel and the letter y, - it will match one or more of these patterns. + If the Discord name contains a vowel and the letter y, it will match one or more of these patterns. + Only the most recently matched pattern will apply the changes. """ expressions = [ diff --git a/bot/seasons/easter/egghead_quiz.py b/bot/seasons/easter/egghead_quiz.py index f479504c..0b175bf1 100644 --- a/bot/seasons/easter/egghead_quiz.py +++ b/bot/seasons/easter/egghead_quiz.py @@ -38,7 +38,7 @@ class EggheadQuiz(commands.Cog): @commands.command(aliases=["eggheadquiz", "easterquiz"]) async def eggquiz(self, ctx: commands.Context) -> None: """ - Gives a random quiz question, waits 30 seconds and then outputs the answer + Gives a random quiz question, waits 30 seconds and then outputs the answer. Also informs of the percentages and votes of each option """ diff --git a/bot/seasons/evergreen/8bitify.py b/bot/seasons/evergreen/8bitify.py index 54db71db..60062fc1 100644 --- a/bot/seasons/evergreen/8bitify.py +++ b/bot/seasons/evergreen/8bitify.py @@ -13,17 +13,17 @@ class EightBitify(commands.Cog): @staticmethod def pixelate(image: Image) -> Image: - """Takes an image and pixelates it""" + """Takes an image and pixelates it.""" return image.resize((32, 32)).resize((1024, 1024)) @staticmethod def quantize(image: Image) -> Image: - """Reduces colour palette to 256 colours""" + """Reduces colour palette to 256 colours.""" return image.quantize(colors=32) @commands.command(name="8bitify") async def eightbit_command(self, ctx: commands.Context) -> None: - """Pixelates your avatar and changes the palette to an 8bit one""" + """Pixelates your avatar and changes the palette to an 8bit one.""" async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() avatar = Image.open(BytesIO(image_bytes)) diff --git a/bot/seasons/evergreen/minesweeper.py b/bot/seasons/evergreen/minesweeper.py index 9dadb9f0..015b09df 100644 --- a/bot/seasons/evergreen/minesweeper.py +++ b/bot/seasons/evergreen/minesweeper.py @@ -175,7 +175,7 @@ class Minesweeper(commands.Cog): @commands.dm_only() @minesweeper_group.command(name="flag") async def flag_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None: - """Place multiple flags on the board""" + """Place multiple flags on the board.""" board: GameBoard = self.games[ctx.author.id].revealed for x, y in coordinates: if board[y][x] == "hidden": @@ -185,14 +185,14 @@ class Minesweeper(commands.Cog): @staticmethod def reveal_bombs(revealed: GameBoard, board: GameBoard) -> None: - """Reveals all the bombs""" + """Reveals all the bombs.""" for y, row in enumerate(board): for x, cell in enumerate(row): if cell == "bomb": revealed[y][x] = cell async def lost(self, ctx: commands.Context) -> None: - """The player lost the game""" + """The player lost the game.""" game = self.games[ctx.author.id] self.reveal_bombs(game.revealed, game.board) await ctx.author.send(":fire: You lost! :fire:") @@ -200,7 +200,7 @@ class Minesweeper(commands.Cog): await game.chat_msg.channel.send(f":fire: {ctx.author.mention} just lost Minesweeper! :fire:") async def won(self, ctx: commands.Context) -> None: - """The player won the game""" + """The player won the game.""" game = self.games[ctx.author.id] await ctx.author.send(":tada: You won! :tada:") if game.activated_on_server: @@ -252,7 +252,7 @@ class Minesweeper(commands.Cog): @commands.dm_only() @minesweeper_group.command(name="reveal") async def reveal_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None: - """Reveal multiple cells""" + """Reveal multiple cells.""" game = self.games[ctx.author.id] revealed: GameBoard = game.revealed board: GameBoard = game.board diff --git a/bot/seasons/evergreen/showprojects.py b/bot/seasons/evergreen/showprojects.py index 2804bdbe..d41132aa 100644 --- a/bot/seasons/evergreen/showprojects.py +++ b/bot/seasons/evergreen/showprojects.py @@ -9,7 +9,7 @@ log = logging.getLogger(__name__) class ShowProjects(commands.Cog): - """Cog that reacts to posts in the #show-your-projects""" + """Cog that reacts to posts in the #show-your-projects.""" def __init__(self, bot: commands.Bot): self.bot = bot diff --git a/bot/seasons/evergreen/snakes/utils.py b/bot/seasons/evergreen/snakes/utils.py index 24e71227..76809bd4 100644 --- a/bot/seasons/evergreen/snakes/utils.py +++ b/bot/seasons/evergreen/snakes/utils.py @@ -13,6 +13,8 @@ from PIL.ImageDraw import ImageDraw from discord import File, Member, Reaction from discord.ext.commands import Cog, Context +from bot.constants import Roles + SNAKE_RESOURCES = Path("bot/resources/snakes").absolute() h1 = r'''``` @@ -412,7 +414,6 @@ class SnakeAndLaddersGame: "**Snakes and Ladders**: A new game is about to start!", file=File( str(SNAKE_RESOURCES / "snakes_and_ladders" / "banner.jpg"), - # os.path.join("bot", "resources", "snakes", "snakes_and_ladders", "banner.jpg"), filename='Snakes and Ladders.jpg' ) ) @@ -435,8 +436,9 @@ class SnakeAndLaddersGame: if reaction.emoji == JOIN_EMOJI: await self.player_join(user) elif reaction.emoji == CANCEL_EMOJI: - if self.ctx.author == user: - await self.cancel_game(user) + if user == self.author or (self._is_moderator(user) and user not in self.players): + # Allow game author or non-playing moderation staff to cancel a waiting game + await self.cancel_game() return else: await self.player_leave(user) @@ -451,7 +453,7 @@ class SnakeAndLaddersGame: except asyncio.TimeoutError: log.debug("Snakes and Ladders timed out waiting for a reaction") - self.cancel_game(self.author) + await self.cancel_game() return # We're done, no reactions for the last 5 minutes async def _add_player(self, user: Member) -> None: @@ -492,16 +494,12 @@ class SnakeAndLaddersGame: """ Handle players leaving the game. - Leaving is prevented if the user initiated the game or if they weren't part of it in the - first place. + Leaving is prevented if the user wasn't part of the game. + + If the number of players reaches 0, the game is terminated. In this case, a sentinel boolean + is returned True to prevent a game from continuing after it's destroyed. """ - if user == self.author: - await self.channel.send( - user.mention + " You are the author, and cannot leave the game. Execute " - "`sal cancel` to cancel the game.", - delete_after=10 - ) - return + is_surrendered = False # Sentinel value to assist with stopping a surrendered game for p in self.players: if user == p: self.players.remove(p) @@ -512,11 +510,10 @@ class SnakeAndLaddersGame: delete_after=10 ) - if self.state != 'waiting' and len(self.players) == 1: + if self.state != 'waiting' and len(self.players) == 0: await self.channel.send("**Snakes and Ladders**: The game has been surrendered!") + is_surrendered = True self._destruct() - return - await self.channel.send(user.mention + " You are not in the match.", delete_after=10) async def cancel_game(self, user: Member) -> None: """Allow the game author to cancel the running game.""" @@ -530,21 +527,16 @@ class SnakeAndLaddersGame: """ Allow the game author to begin the game. - The game cannot be started if there aren't enough players joined or if the game is in a - waiting state. + The game cannot be started if the game is in a waiting state. """ if not user == self.author: await self.channel.send(user.mention + " Only the author of the game can start it.", delete_after=10) return - if len(self.players) < 1: - await self.channel.send( - user.mention + " A minimum of 2 players is required to start the game.", - delete_after=10 - ) - return + if not self.state == 'waiting': await self.channel.send(user.mention + " The game cannot be started at this time.", delete_after=10) return + self.state = 'starting' player_list = ', '.join(user.mention for user in self.players) await self.channel.send("**Snakes and Ladders**: The game is starting!\nPlayers: " + player_list) @@ -565,8 +557,6 @@ class SnakeAndLaddersGame: self.state = 'roll' for user in self.players: self.round_has_rolled[user.id] = False - # board_img = Image.open(os.path.join( - # "bot", "resources", "snakes", "snakes_and_ladders", "board.jpg")) board_img = Image.open(str(SNAKE_RESOURCES / "snakes_and_ladders" / "board.jpg")) player_row_size = math.ceil(MAX_PLAYERS / 2) @@ -612,6 +602,7 @@ class SnakeAndLaddersGame: for emoji in GAME_SCREEN_EMOJI: await self.positions.add_reaction(emoji) + is_surrendered = False while True: try: reaction, user = await self.ctx.bot.wait_for( @@ -623,11 +614,12 @@ class SnakeAndLaddersGame: if reaction.emoji == ROLL_EMOJI: await self.player_roll(user) elif reaction.emoji == CANCEL_EMOJI: - if self.ctx.author == user: - await self.cancel_game(user) + if self._is_moderator(user) and user not in self.players: + # Only allow non-playing moderation staff to cancel a running game + await self.cancel_game() return else: - await self.player_leave(user) + is_surrendered = await self.player_leave(user) await self.positions.remove_reaction(reaction.emoji, user) @@ -636,11 +628,14 @@ class SnakeAndLaddersGame: except asyncio.TimeoutError: log.debug("Snakes and Ladders timed out waiting for a reaction") - await self.cancel_game(self.author) + await self.cancel_game() return # We're done, no reactions for the last 5 minutes # Round completed - await self._complete_round() + # Check to see if the game was surrendered before completing the round, without this + # sentinel, the game object would be deleted but the next round still posted into purgatory + if not is_surrendered: + await self._complete_round() async def player_roll(self, user: Member) -> None: """Handle the player's roll.""" @@ -708,3 +703,8 @@ class SnakeAndLaddersGame: if is_reversed: x_level = 9 - x_level return x_level, y_level + + @staticmethod + def _is_moderator(user: Member) -> bool: + """Return True if the user is a Moderator.""" + return any(Roles.moderator == role.id for role in user.roles) diff --git a/bot/seasons/halloween/candy_collection.py b/bot/seasons/halloween/candy_collection.py index 65fa9af8..64da7ced 100644 --- a/bot/seasons/halloween/candy_collection.py +++ b/bot/seasons/halloween/candy_collection.py @@ -122,15 +122,16 @@ class CandyCollection(commands.Cog): async def ten_recent_msg(self) -> List[int]: """Get the last 10 messages sent in the channel.""" ten_recent = [] - recent_msg = max(message.id for message - in self.bot._connection._messages - if message.channel.id == Channels.seasonalbot_chat) + recent_msg_id = max( + message.id for message in self.bot._connection._messages + if message.channel.id == Channels.seasonalbot_chat + ) channel = await self.hacktober_channel() - ten_recent.append(recent_msg.id) + ten_recent.append(recent_msg_id) for i in range(9): - o = discord.Object(id=recent_msg.id + i) + o = discord.Object(id=recent_msg_id + i) msg = await next(channel.history(limit=1, before=o)) ten_recent.append(msg.id) diff --git a/bot/seasons/halloween/hacktoberstats.py b/bot/seasons/halloween/hacktoberstats.py index b6b5a900..5687a5c7 100644 --- a/bot/seasons/halloween/hacktoberstats.py +++ b/bot/seasons/halloween/hacktoberstats.py @@ -1,10 +1,10 @@ import json import logging import re -import typing from collections import Counter from datetime import datetime from pathlib import Path +from typing import List, Tuple import aiohttp import discord @@ -12,6 +12,9 @@ from discord.ext import commands log = logging.getLogger(__name__) +CURRENT_YEAR = datetime.now().year # Used to construct GH API query +PRS_FOR_SHIRT = 4 # Minimum number of PRs before a shirt is awarded + class HacktoberStats(commands.Cog): """Hacktoberfest statistics Cog.""" @@ -21,12 +24,8 @@ class HacktoberStats(commands.Cog): self.link_json = Path("bot/resources/github_links.json") self.linked_accounts = self.load_linked_users() - @commands.group( - name='hacktoberstats', - aliases=('hackstats',), - invoke_without_command=True - ) - async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None): + @commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True) + async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None) -> None: """ Display an embed for a user's Hacktoberfest contributions. @@ -43,8 +42,8 @@ class HacktoberStats(commands.Cog): else: msg = ( f"{author_mention}, you have not linked a GitHub account\n\n" - f"You can link your GitHub account using:\n```{ctx.prefix}stats link github_username```\n" - f"Or query GitHub stats directly using:\n```{ctx.prefix}stats github_username```" + f"You can link your GitHub account using:\n```{ctx.prefix}hackstats link github_username```\n" + f"Or query GitHub stats directly using:\n```{ctx.prefix}hackstats github_username```" ) await ctx.send(msg) return @@ -52,7 +51,7 @@ class HacktoberStats(commands.Cog): await self.get_stats(ctx, github_username) @hacktoberstats_group.command(name="link") - async def link_user(self, ctx: commands.Context, github_username: str = None): + 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. @@ -85,7 +84,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") - async def unlink_user(self, ctx: commands.Context): + 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) @@ -99,7 +98,7 @@ class HacktoberStats(commands.Cog): self.save_linked_users() - def load_linked_users(self) -> typing.Dict: + def load_linked_users(self) -> dict: """ Load list of linked users from local JSON file. @@ -122,7 +121,7 @@ class HacktoberStats(commands.Cog): logging.info(f"Linked account log: '{self.link_json}' does not exist") return {} - def save_linked_users(self): + def save_linked_users(self) -> None: """ Save list of linked users to local JSON file. @@ -139,7 +138,7 @@ class HacktoberStats(commands.Cog): json.dump(self.linked_accounts, fID, default=str) logging.info(f"linked_accounts saved to '{self.link_json}'") - async def get_stats(self, ctx: commands.Context, github_username: str): + async def get_stats(self, ctx: commands.Context, github_username: str) -> None: """ Query GitHub's API for PRs created by a GitHub user during the month of October. @@ -158,18 +157,18 @@ class HacktoberStats(commands.Cog): else: await ctx.send(f"No October GitHub contributions found for '{github_username}'") - def build_embed(self, github_username: str, prs: typing.List[dict]) -> discord.Embed: + def build_embed(self, github_username: str, prs: List[dict]) -> discord.Embed: """Return a stats embed built from github_username's PRs.""" logging.info(f"Building Hacktoberfest embed for GitHub user: '{github_username}'") pr_stats = self._summarize_prs(prs) n = pr_stats['n_prs'] - if n >= 5: + if n >= PRS_FOR_SHIRT: shirtstr = f"**{github_username} has earned a tshirt!**" - elif n == 4: + elif n == PRS_FOR_SHIRT - 1: shirtstr = f"**{github_username} is 1 PR away from a tshirt!**" else: - shirtstr = f"**{github_username} is {5 - n} PRs away from a tshirt!**" + shirtstr = f"**{github_username} is {PRS_FOR_SHIRT - n} PRs away from a tshirt!**" stats_embed = discord.Embed( title=f"{github_username}'s Hacktoberfest", @@ -186,7 +185,7 @@ class HacktoberStats(commands.Cog): stats_embed.set_author( name="Hacktoberfest", url="https://hacktoberfest.digitalocean.com", - icon_url="https://hacktoberfest.digitalocean.com/assets/logo-hacktoberfest.png" + icon_url="https://hacktoberfest.digitalocean.com/pretty_logo.png" ) stats_embed.add_field( name="Top 5 Repositories:", @@ -197,7 +196,7 @@ class HacktoberStats(commands.Cog): return stats_embed @staticmethod - async def get_october_prs(github_username: str) -> typing.List[dict]: + async def get_october_prs(github_username: str) -> List[dict]: """ Query GitHub's API for PRs created during the month of October by github_username. @@ -219,7 +218,7 @@ class HacktoberStats(commands.Cog): not_label = "invalid" action_type = "pr" is_query = f"public+author:{github_username}" - date_range = "2018-10-01..2018-10-31" + date_range = f"{CURRENT_YEAR}-10-01..{CURRENT_YEAR}-10-31" per_page = "300" query_url = ( f"{base_url}" @@ -274,7 +273,7 @@ class HacktoberStats(commands.Cog): return re.findall(exp, in_url)[0] @staticmethod - def _summarize_prs(prs: typing.List[dict]) -> typing.Dict: + def _summarize_prs(prs: List[dict]) -> dict: """ Generate statistics from an input list of PR dictionaries, as output by get_october_prs. @@ -288,7 +287,7 @@ class HacktoberStats(commands.Cog): return {"n_prs": len(prs), "top5": Counter(contributed_repos).most_common(5)} @staticmethod - def _build_top5str(stats: typing.List[tuple]) -> str: + def _build_top5str(stats: List[tuple]) -> str: """ Build a string from the Top 5 contributions that is compatible with a discord.Embed field. @@ -316,7 +315,7 @@ class HacktoberStats(commands.Cog): return "contributions" @staticmethod - def _author_mention_from_context(ctx: commands.Context) -> typing.Tuple: + def _author_mention_from_context(ctx: commands.Context) -> Tuple: """Return stringified Message author ID and mentionable string from commands.Context.""" author_id = str(ctx.message.author.id) author_mention = ctx.message.author.mention @@ -324,7 +323,7 @@ class HacktoberStats(commands.Cog): return author_id, author_mention -def setup(bot): +def setup(bot): # Noqa """Hacktoberstats Cog load.""" bot.add_cog(HacktoberStats(bot)) log.info("HacktoberStats cog loaded") diff --git a/bot/seasons/halloween/monstersurvey.py b/bot/seasons/halloween/monstersurvey.py index cfd3edaf..12e1d022 100644 --- a/bot/seasons/halloween/monstersurvey.py +++ b/bot/seasons/halloween/monstersurvey.py @@ -60,7 +60,7 @@ class MonsterSurvey(Cog): @commands.group( name='monster', - aliases=('ms',) + aliases=('mon',) ) async def monster_group(self, ctx: Context) -> None: """The base voting command. If nothing is called, then it will return an embed.""" diff --git a/bot/seasons/halloween/spookyrating.py b/bot/seasons/halloween/spookyrating.py index 02e6f6b9..7f78f536 100644 --- a/bot/seasons/halloween/spookyrating.py +++ b/bot/seasons/halloween/spookyrating.py @@ -17,7 +17,7 @@ with Path("bot/resources/halloween/spooky_rating.json").open() as file: class SpookyRating(commands.Cog): - """A cog for calculating one's spooky rating""" + """A cog for calculating one's spooky rating.""" def __init__(self, bot: commands.Bot): self.bot = bot @@ -1,10 +1,11 @@ [flake8] max-line-length=120 application_import_names=bot +docstring-convention=all ignore= P102,B311,W503,E226,S311, # Missing Docstrings - D100,D104,D107, + D100,D104,D105,D107, # Docstring Whitespace D203,D212,D214,D215, # Docstring Quotes |