diff options
Diffstat (limited to 'bot/exts')
| -rw-r--r-- | bot/exts/christmas/advent_of_code/_helpers.py | 8 | ||||
| -rw-r--r-- | bot/exts/evergreen/avatar_modification/avatar_modify.py | 44 | ||||
| -rw-r--r-- | bot/exts/evergreen/githubinfo.py | 9 | ||||
| -rw-r--r-- | bot/exts/evergreen/help.py | 4 | ||||
| -rw-r--r-- | bot/exts/evergreen/issues.py | 6 | ||||
| -rw-r--r-- | bot/exts/evergreen/movie.py | 11 | ||||
| -rw-r--r-- | bot/exts/evergreen/snakes/_converter.py | 2 | ||||
| -rw-r--r-- | bot/exts/evergreen/stackoverflow.py | 88 | ||||
| -rw-r--r-- | bot/exts/evergreen/tic_tac_toe.py | 7 | ||||
| -rw-r--r-- | bot/exts/evergreen/trivia_quiz.py | 31 | ||||
| -rw-r--r-- | bot/exts/evergreen/wikipedia.py | 17 | ||||
| -rw-r--r-- | bot/exts/evergreen/wolfram.py | 34 | ||||
| -rw-r--r-- | bot/exts/halloween/hacktoberstats.py | 25 | ||||
| -rw-r--r-- | bot/exts/halloween/scarymovie.py | 16 | ||||
| -rw-r--r-- | bot/exts/pride/pride_leader.py | 2 | ||||
| -rw-r--r-- | bot/exts/valentines/lovecalculator.py | 49 | ||||
| -rw-r--r-- | bot/exts/valentines/movie_generator.py | 5 | 
17 files changed, 212 insertions, 146 deletions
| diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index 96de90c4..e26a17ca 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -67,7 +67,7 @@ class UnexpectedResponseStatus(aiohttp.ClientError):      """Raised when an unexpected redirect was detected.""" -class FetchingLeaderboardFailed(Exception): +class FetchingLeaderboardFailedError(Exception):      """Raised when one or more leaderboards could not be fetched at all.""" @@ -210,7 +210,7 @@ async def _fetch_leaderboard_data() -> typing.Dict[str, typing.Any]:              except UnexpectedRedirect:                  if cookies["session"] == AdventOfCode.fallback_session:                      log.error("It seems like the fallback cookie has expired!") -                    raise FetchingLeaderboardFailed from None +                    raise FetchingLeaderboardFailedError from None                  # If we're here, it means that the original session did not                  # work. Let's fall back to the fallback session. @@ -218,7 +218,7 @@ async def _fetch_leaderboard_data() -> typing.Dict[str, typing.Any]:                  continue              except aiohttp.ClientError:                  # Don't retry, something unexpected is wrong and it may not be the session. -                raise FetchingLeaderboardFailed from None +                raise FetchingLeaderboardFailedError from None              else:                  # Get the participants and store their current count.                  board_participants = raw_data["members"] @@ -227,7 +227,7 @@ async def _fetch_leaderboard_data() -> typing.Dict[str, typing.Any]:                  break          else:              log.error(f"reached 'unreachable' state while fetching board `{leaderboard.id}`.") -            raise FetchingLeaderboardFailed +            raise FetchingLeaderboardFailedError      log.info(f"Fetched leaderboard information for {len(participants)} participants")      return participants diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index 185cdb38..765c316e 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -9,7 +9,6 @@ from concurrent.futures import ThreadPoolExecutor  from pathlib import Path  import discord -from aiohttp import client_exceptions  from discord.ext import commands  from bot.bot import Bot @@ -273,37 +272,6 @@ class AvatarModify(commands.Cog):              await self.send_pride_image(ctx, image_bytes, pixels, flag, option)      @prideavatar.command() -    async def image(self, ctx: commands.Context, url: str, option: str = "lgbt", pixels: int = 64) -> None: -        """ -        This surrounds the image specified by the URL with a border of a specified LGBT flag. - -        This defaults to the LGBT rainbow flag if none is given. -        The amount of pixels can be given which determines the thickness of the flag border. -        This has a maximum of 512px and defaults to a 64px border. -        The full image is 1024x1024. -        """ -        option = option.lower() -        pixels = max(0, min(512, pixels)) -        flag = GENDER_OPTIONS.get(option) -        if flag is None: -            await ctx.send("I don't have that flag!") -            return - -        async with ctx.typing(): -            try: -                async with self.bot.http_session.get(url) as response: -                    if response.status != 200: -                        await ctx.send("Bad response from provided URL!") -                        return -                    image_bytes = await response.read() -            except client_exceptions.ClientConnectorError: -                raise commands.BadArgument("Cannot connect to provided URL!") -            except client_exceptions.InvalidURL: -                raise commands.BadArgument("Invalid URL!") - -            await self.send_pride_image(ctx, image_bytes, pixels, flag, option) - -    @prideavatar.command()      async def flags(self, ctx: commands.Context) -> None:          """This lists the flags that can be used with the prideavatar command."""          choices = sorted(set(GENDER_OPTIONS.values())) @@ -320,12 +288,9 @@ class AvatarModify(commands.Cog):          root_aliases=("spookyavatar", "spookify", "savatar"),          brief="Spookify an user's avatar."      ) -    async def spookyavatar(self, ctx: commands.Context, member: discord.Member = None) -> None: -        """This "spookifies" the given user's avatar, with a random *spooky* effect.""" -        if member is None: -            member = ctx.author - -        user = await self._fetch_user(member.id) +    async def spookyavatar(self, ctx: commands.Context) -> None: +        """This "spookifies" the user's avatar, with a random *spooky* effect.""" +        user = await self._fetch_user(ctx.author.id)          if not user:              await ctx.send(f"{Emojis.cross_mark} Could not get user info.")              return @@ -333,7 +298,7 @@ class AvatarModify(commands.Cog):          async with ctx.typing():              image_bytes = await user.avatar_url_as(size=1024).read() -            file_name = file_safe_name("spooky_avatar", member.display_name) +            file_name = file_safe_name("spooky_avatar", ctx.author.display_name)              file = await in_executor(                  PfpEffects.apply_effect, @@ -346,7 +311,6 @@ class AvatarModify(commands.Cog):                  title="Is this you or am I just really paranoid?",                  colour=Colours.soft_red              ) -            embed.set_author(name=member.name, icon_url=member.avatar_url)              embed.set_image(url=f"attachment://{file_name}")              embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) diff --git a/bot/exts/evergreen/githubinfo.py b/bot/exts/evergreen/githubinfo.py index 27e607e5..d29f3aa9 100644 --- a/bot/exts/evergreen/githubinfo.py +++ b/bot/exts/evergreen/githubinfo.py @@ -1,7 +1,7 @@  import logging  import random  from datetime import datetime -from urllib.parse import quote +from urllib.parse import quote, quote_plus  import discord  from discord.ext import commands @@ -37,7 +37,7 @@ class GithubInfo(commands.Cog):      async def github_user_info(self, ctx: commands.Context, username: str) -> None:          """Fetches a user's GitHub information."""          async with ctx.typing(): -            user_data = await self.fetch_data(f"{GITHUB_API_URL}/users/{username}") +            user_data = await self.fetch_data(f"{GITHUB_API_URL}/users/{quote_plus(username)}")              # User_data will not have a message key if the user exists              if "message" in user_data: @@ -91,7 +91,10 @@ class GithubInfo(commands.Cog):              )              if user_data["type"] == "User": -                embed.add_field(name="Gists", value=f"[{gists}](https://gist.github.com/{quote(username, safe='')})") +                embed.add_field( +                    name="Gists", +                    value=f"[{gists}](https://gist.github.com/{quote_plus(username, safe='')})" +                )                  embed.add_field(                      name=f"Organization{'s' if len(orgs)!=1 else ''}", diff --git a/bot/exts/evergreen/help.py b/bot/exts/evergreen/help.py index 3c9ba4d2..bfb5db17 100644 --- a/bot/exts/evergreen/help.py +++ b/bot/exts/evergreen/help.py @@ -8,7 +8,7 @@ from typing import List, NamedTuple, Union  from discord import Colour, Embed, HTTPException, Message, Reaction, User  from discord.ext import commands  from discord.ext.commands import CheckFailure, Cog as DiscordCog, Command, Context -from fuzzywuzzy import fuzz, process +from rapidfuzz import process  from bot import constants  from bot.bot import Bot @@ -159,7 +159,7 @@ class HelpSession:          # Combine command and cog names          choices = list(self._bot.all_commands) + list(self._bot.cogs) -        result = process.extractBests(query, choices, scorer=fuzz.ratio, score_cutoff=90) +        result = process.extract(query, choices, score_cutoff=90)          raise HelpQueryNotFound(f'Query "{query}" not found.', dict(result)) diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index b67aa4a6..00810de8 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -135,7 +135,7 @@ class Issues(commands.Cog):          # need from the initial API call.          if "issues" in json_data["html_url"]:              if json_data.get("state") == "open": -                emoji = Emojis.issue +                emoji = Emojis.issue_open              else:                  emoji = Emojis.issue_closed @@ -149,10 +149,10 @@ class Issues(commands.Cog):                  if pull_data["draft"]:                      emoji = Emojis.pull_request_draft                  elif pull_data["state"] == "open": -                    emoji = Emojis.pull_request +                    emoji = Emojis.pull_request_open                  # When 'merged_at' is not None, this means that the state of the PR is merged                  elif pull_data["merged_at"] is not None: -                    emoji = Emojis.merge +                    emoji = Emojis.pull_request_merged                  else:                      emoji = Emojis.pull_request_closed diff --git a/bot/exts/evergreen/movie.py b/bot/exts/evergreen/movie.py index 10638aea..c6af4bcd 100644 --- a/bot/exts/evergreen/movie.py +++ b/bot/exts/evergreen/movie.py @@ -2,7 +2,6 @@ import logging  import random  from enum import Enum  from typing import Any, Dict, List, Tuple -from urllib.parse import urlencode  from aiohttp import ClientSession  from discord import Embed @@ -121,10 +120,10 @@ class Movie(Cog):              "with_genres": genre_id          } -        url = BASE_URL + "discover/movie?" + urlencode(params) +        url = BASE_URL + "discover/movie"          # Make discover request to TMDB, return result -        async with client.get(url) as resp: +        async with client.get(url, params=params) as resp:              return await resp.json()      async def get_pages(self, client: ClientSession, movies: Dict[str, Any], amount: int) -> List[Tuple[str, str]]: @@ -142,9 +141,11 @@ class Movie(Cog):      async def get_movie(self, client: ClientSession, movie: int) -> Dict:          """Get Movie by movie ID from TMDB. Return result dictionary.""" -        url = BASE_URL + f"movie/{movie}?" + urlencode(MOVIE_PARAMS) +        if not isinstance(movie, int): +            raise ValueError("Error while fetching movie from TMDB, movie argument must be integer. ") +        url = BASE_URL + f"movie/{movie}" -        async with client.get(url) as resp: +        async with client.get(url, params=MOVIE_PARAMS) as resp:              return await resp.json()      async def create_page(self, movie: Dict[str, Any]) -> Tuple[str, str]: diff --git a/bot/exts/evergreen/snakes/_converter.py b/bot/exts/evergreen/snakes/_converter.py index 26bde611..c8d1909b 100644 --- a/bot/exts/evergreen/snakes/_converter.py +++ b/bot/exts/evergreen/snakes/_converter.py @@ -5,7 +5,7 @@ from typing import Iterable, List  import discord  from discord.ext.commands import Context, Converter -from fuzzywuzzy import fuzz +from rapidfuzz import fuzz  from bot.exts.evergreen.snakes._utils import SNAKE_RESOURCES  from bot.utils import disambiguate diff --git a/bot/exts/evergreen/stackoverflow.py b/bot/exts/evergreen/stackoverflow.py new file mode 100644 index 00000000..40f149c9 --- /dev/null +++ b/bot/exts/evergreen/stackoverflow.py @@ -0,0 +1,88 @@ +import logging +from html import unescape +from urllib.parse import quote_plus + +from discord import Embed, HTTPException +from discord.ext import commands + +from bot import bot +from bot.constants import Colours, Emojis + +logger = logging.getLogger(__name__) + +BASE_URL = "https://api.stackexchange.com/2.2/search/advanced" +SO_PARAMS = { +    "order": "desc", +    "sort": "activity", +    "site": "stackoverflow" +} +SEARCH_URL = "https://stackoverflow.com/search?q={query}" +ERR_EMBED = Embed( +    title="Error in fetching results from Stackoverflow", +    description=( +        "Sorry, there was en error while trying to fetch data from the Stackoverflow website. Please try again in some " +        "time. If this issue persists, please contact the staff or send a message in #dev-contrib." +    ), +    color=Colours.soft_red +) + + +class Stackoverflow(commands.Cog): +    """Contains command to interact with stackoverflow from discord.""" + +    def __init__(self, bot: bot.Bot): +        self.bot = bot + +    @commands.command(aliases=["so"]) +    @commands.cooldown(1, 15, commands.cooldowns.BucketType.user) +    async def stackoverflow(self, ctx: commands.Context, *, search_query: str) -> None: +        """Sends the top 5 results of a search query from stackoverflow.""" +        params = SO_PARAMS | {"q": search_query} +        async with self.bot.http_session.get(url=BASE_URL, params=params) as response: +            if response.status == 200: +                data = await response.json() +            else: +                logger.error(f'Status code is not 200, it is {response.status}') +                await ctx.send(embed=ERR_EMBED) +                return +        if not data['items']: +            no_search_result = Embed( +                title=f"No search results found for {search_query}", +                color=Colours.soft_red +            ) +            await ctx.send(embed=no_search_result) +            return + +        top5 = data["items"][:5] +        encoded_search_query = quote_plus(search_query) +        embed = Embed( +            title="Search results - Stackoverflow", +            url=SEARCH_URL.format(query=encoded_search_query), +            description=f"Here are the top {len(top5)} results:", +            color=Colours.orange +        ) +        for item in top5: +            embed.add_field( +                name=unescape(item['title']), +                value=( +                    f"[{Emojis.reddit_upvote} {item['score']}    " +                    f"{Emojis.stackoverflow_views} {item['view_count']}     " +                    f"{Emojis.reddit_comments} {item['answer_count']}   " +                    f"{Emojis.stackoverflow_tag} {', '.join(item['tags'][:3])}]" +                    f"({item['link']})" +                ), +                inline=False) +        embed.set_footer(text="View the original link for more results.") +        try: +            await ctx.send(embed=embed) +        except HTTPException: +            search_query_too_long = Embed( +                title="Your search query is too long, please try shortening your search query", +                color=Colours.soft_red +            ) +            await ctx.send(embed=search_query_too_long) + + +def setup(bot: bot.Bot) -> None: +    """Load the Stackoverflow Cog.""" +    bot.add_cog(Stackoverflow(bot)) diff --git a/bot/exts/evergreen/tic_tac_toe.py b/bot/exts/evergreen/tic_tac_toe.py index 48e8e142..164e056d 100644 --- a/bot/exts/evergreen/tic_tac_toe.py +++ b/bot/exts/evergreen/tic_tac_toe.py @@ -318,9 +318,14 @@ class TicTacToe(Cog):              return          game = self.games[game_id - 1] +        if game.draw: +            description = f"{game.players[0]} vs {game.players[1]} (draw)\n\n{game.format_board()}" +        else: +            description = f"{game.winner} :trophy: vs {game.loser}\n\n{game.format_board()}" +          embed = discord.Embed(              title=f"Match #{game_id} Game Board", -            description=f"{game.winner} :trophy: vs {game.loser}\n\n{game.format_board()}" +            description=description,          )          await ctx.send(embed=embed) diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py index a8d10afd..bc25cbf7 100644 --- a/bot/exts/evergreen/trivia_quiz.py +++ b/bot/exts/evergreen/trivia_quiz.py @@ -9,7 +9,7 @@ from typing import Callable, List, Optional  import discord  from discord.ext import commands -from fuzzywuzzy import fuzz +from rapidfuzz import fuzz  from bot.bot import Bot  from bot.constants import Colours, NEGATIVE_REPLIES, Roles @@ -17,8 +17,8 @@ from bot.constants import Colours, NEGATIVE_REPLIES, Roles  logger = logging.getLogger(__name__)  DEFAULT_QUESTION_LIMIT = 6 -STANDARD_VARIATION_TOLERANCE = 83 -DYNAMICALLY_GEN_VARIATION_TOLERANCE = 95 +STANDARD_VARIATION_TOLERANCE = 88 +DYNAMICALLY_GEN_VARIATION_TOLERANCE = 97  WRONG_ANS_RESPONSE = [      "No one answered correctly!", @@ -210,6 +210,8 @@ class TriviaQuiz(commands.Cog):              "retro": "Questions related to retro gaming.",              "math": "General questions about mathematics ranging from grade 8 to grade 12.",              "science": "Put your understanding of science to the test!", +            "cs": "A large variety of computer science questions.", +            "python": "Trivia on our amazing language, Python!",          }      @staticmethod @@ -225,10 +227,12 @@ class TriviaQuiz(commands.Cog):          Start a quiz!          Questions for the quiz can be selected from the following categories: -        - general: Test your general knowledge. (default) +        - general: Test your general knowledge.          - retro: Questions related to retro gaming.          - math: General questions about mathematics ranging from grade 8 to grade 12.          - science: Put your understanding of science to the test! +        - cs: A large variety of computer science questions. +        - python: Trivia on our amazing language, Python!          (More to come!)          """ @@ -290,7 +294,7 @@ class TriviaQuiz(commands.Cog):              start_embed = self.make_start_embed(category)              await ctx.send(embed=start_embed)  # send an embed with the rules -            await asyncio.sleep(1) +            await asyncio.sleep(5)          done_question = []          hint_no = 0 @@ -430,21 +434,18 @@ class TriviaQuiz(commands.Cog):          """Generate a starting/introduction embed for the quiz."""          start_embed = discord.Embed(              colour=Colours.blue, -            title="Quiz game starting!", +            title="A quiz game is starting!",              description=( -                f"This game consists of {self.question_limit + 1} questions.\n" -                "**Rules: **No cheating and have fun!\n" +                f"This game consists of {self.question_limit + 1} questions.\n\n" +                "**Rules: **\n" +                "1. Only enclose your answer in backticks when the question tells you to.\n" +                "2. If the question specifies an answer format, follow it or else it won't be accepted.\n" +                "3. You have 30s per question. Points for each question reduces by 25 after 10s or after a hint.\n" +                "4. No cheating and have fun!\n\n"                  f"**Category**: {category}"              ),          ) -        start_embed.set_footer( -            text=( -                "Points for each question reduces by 25 after 10s or after a hint. " -                "Total time is 30s per question" -            ) -        ) -          return start_embed      @staticmethod diff --git a/bot/exts/evergreen/wikipedia.py b/bot/exts/evergreen/wikipedia.py index 83937438..7b96cb7b 100644 --- a/bot/exts/evergreen/wikipedia.py +++ b/bot/exts/evergreen/wikipedia.py @@ -13,9 +13,18 @@ from bot.utils import LinePaginator  log = logging.getLogger(__name__)  SEARCH_API = ( -    "https://en.wikipedia.org/w/api.php?action=query&list=search&prop=info&inprop=url&utf8=&" -    "format=json&origin=*&srlimit={number_of_results}&srsearch={string}" +    "https://en.wikipedia.org/w/api.php"  ) +WIKI_PARAMS = { +    "action": "query", +    "list": "search", +    "prop": "info", +    "inprop": "url", +    "utf8": "", +    "format": "json", +    "origin": "*", + +}  WIKI_THUMBNAIL = (      "https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg"      "/330px-Wikipedia-logo-v2.svg.png" @@ -35,8 +44,8 @@ class WikipediaSearch(commands.Cog):      async def wiki_request(self, channel: TextChannel, search: str) -> Optional[List[str]]:          """Search wikipedia search string and return formatted first 10 pages found.""" -        url = SEARCH_API.format(number_of_results=10, string=search) -        async with self.bot.http_session.get(url=url) as resp: +        params = WIKI_PARAMS | {"srlimit": 10, "srsearch": search} +        async with self.bot.http_session.get(url=SEARCH_API, params=params) as resp:              if resp.status == 200:                  raw_data = await resp.json()                  number_of_results = raw_data["query"]["searchinfo"]["totalhits"] diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py index d23afd6f..26674d37 100644 --- a/bot/exts/evergreen/wolfram.py +++ b/bot/exts/evergreen/wolfram.py @@ -1,7 +1,7 @@  import logging  from io import BytesIO  from typing import Callable, List, Optional, Tuple -from urllib import parse +from urllib.parse import urlencode  import arrow  import discord @@ -17,7 +17,7 @@ log = logging.getLogger(__name__)  APPID = Wolfram.key  DEFAULT_OUTPUT_FORMAT = "JSON" -QUERY = "http://api.wolframalpha.com/v2/{request}?{data}" +QUERY = "http://api.wolframalpha.com/v2/{request}"  WOLF_IMAGE = "https://www.symbols.com/gi.php?type=1&id=2886&i=1"  MAX_PODS = 20 @@ -108,7 +108,7 @@ def custom_cooldown(*ignore: List[int]) -> Callable:  async def get_pod_pages(ctx: Context, bot: Bot, query: str) -> Optional[List[Tuple]]:      """Get the Wolfram API pod pages for the provided query."""      async with ctx.typing(): -        url_str = parse.urlencode({ +        params = {              "input": query,              "appid": APPID,              "output": DEFAULT_OUTPUT_FORMAT, @@ -116,27 +116,27 @@ async def get_pod_pages(ctx: Context, bot: Bot, query: str) -> Optional[List[Tup              "location": "the moon",              "latlong": "0.0,0.0",              "ip": "1.1.1.1" -        }) -        request_url = QUERY.format(request="query", data=url_str) +        } +        request_url = QUERY.format(request="query") -        async with bot.http_session.get(request_url) as response: +        async with bot.http_session.get(url=request_url, params=params) as response:              json = await response.json(content_type="text/plain")          result = json["queryresult"] - +        log_full_url = f"{request_url}?{urlencode(params)}"          if result["error"]:              # API key not set up correctly              if result["error"]["msg"] == "Invalid appid":                  message = "Wolfram API key is invalid or missing."                  log.warning(                      "API key seems to be missing, or invalid when " -                    f"processing a wolfram request: {url_str}, Response: {json}" +                    f"processing a wolfram request: {log_full_url}, Response: {json}"                  )                  await send_embed(ctx, message)                  return              message = "Something went wrong internally with your request, please notify staff!" -            log.warning(f"Something went wrong getting a response from wolfram: {url_str}, Response: {json}") +            log.warning(f"Something went wrong getting a response from wolfram: {log_full_url}, Response: {json}")              await send_embed(ctx, message)              return @@ -172,18 +172,18 @@ class Wolfram(Cog):      @custom_cooldown(*STAFF_ROLES)      async def wolfram_command(self, ctx: Context, *, query: str) -> None:          """Requests all answers on a single image, sends an image of all related pods.""" -        url_str = parse.urlencode({ +        params = {              "i": query,              "appid": APPID,              "location": "the moon",              "latlong": "0.0,0.0",              "ip": "1.1.1.1" -        }) -        query = QUERY.format(request="simple", data=url_str) +        } +        request_url = QUERY.format(request="simple")          # Give feedback that the bot is working.          async with ctx.typing(): -            async with self.bot.http_session.get(query) as response: +            async with self.bot.http_session.get(url=request_url, params=params) as response:                  status = response.status                  image_bytes = await response.read() @@ -257,18 +257,18 @@ class Wolfram(Cog):      @custom_cooldown(*STAFF_ROLES)      async def wolfram_short_command(self, ctx: Context, *, query: str) -> None:          """Requests an answer to a simple question.""" -        url_str = parse.urlencode({ +        params = {              "i": query,              "appid": APPID,              "location": "the moon",              "latlong": "0.0,0.0",              "ip": "1.1.1.1" -        }) -        query = QUERY.format(request="result", data=url_str) +        } +        request_url = QUERY.format(request="result")          # Give feedback that the bot is working.          async with ctx.typing(): -            async with self.bot.http_session.get(query) as response: +            async with self.bot.http_session.get(url=request_url, params=params) as response:                  status = response.status                  response_text = await response.text() diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py index b74e680b..24106a5e 100644 --- a/bot/exts/halloween/hacktoberstats.py +++ b/bot/exts/halloween/hacktoberstats.py @@ -4,21 +4,21 @@ import re  from collections import Counter  from datetime import datetime, timedelta  from typing import List, Optional, Tuple, Union +from urllib.parse import quote_plus  import discord  from async_rediscache import RedisCache  from discord.ext import commands  from bot.bot import Bot -from bot.constants import Channels, Colours, Month, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS -from bot.utils.decorators import in_month, whitelist_override +from bot.constants import Colours, Month, NEGATIVE_REPLIES, Tokens +from bot.utils.decorators import in_month  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  REVIEW_DAYS = 14  # number of days needed after PR can be mature -HACKTOBER_WHITELIST = WHITELISTED_CHANNELS + (Channels.hacktoberfest_2020,)  REQUEST_HEADERS = {"User-Agent": "Python Discord Hacktoberbot"}  # using repo topics API during preview period requires an accept header @@ -44,7 +44,6 @@ class HacktoberStats(commands.Cog):      @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True) -    @whitelist_override(channels=HACKTOBER_WHITELIST)      async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None) -> None:          """          Display an embed for a user's Hacktoberfest contributions. @@ -72,7 +71,6 @@ class HacktoberStats(commands.Cog):      @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @hacktoberstats_group.command(name="link") -    @whitelist_override(channels=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. @@ -96,7 +94,6 @@ class HacktoberStats(commands.Cog):      @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @hacktoberstats_group.command(name="unlink") -    @whitelist_override(channels=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 = self._author_mention_from_context(ctx) @@ -212,24 +209,24 @@ class HacktoberStats(commands.Cog):          None will be returned when the GitHub user was not found.          """          log.info(f"Fetching Hacktoberfest Stats for GitHub user: '{github_username}'") -        base_url = "https://api.github.com/search/issues?q=" +        base_url = "https://api.github.com/search/issues"          action_type = "pr"          is_query = "public"          not_query = "draft"          date_range = f"{CURRENT_YEAR}-09-30T10:00Z..{CURRENT_YEAR}-11-01T12:00Z"          per_page = "300" -        query_url = ( -            f"{base_url}" +        query_params = (              f"+type:{action_type}"              f"+is:{is_query}" -            f"+author:{github_username}" +            f"+author:{quote_plus(github_username)}"              f"+-is:{not_query}"              f"+created:{date_range}"              f"&per_page={per_page}"          ) -        log.debug(f"GitHub query URL generated: {query_url}") -        jsonresp = await self._fetch_url(query_url, REQUEST_HEADERS) +        log.debug(f"GitHub query parameters generated: {query_params}") + +        jsonresp = await self._fetch_url(base_url, REQUEST_HEADERS, {"q": query_params})          if "message" in jsonresp:              # One of the parameters is invalid, short circuit for now              api_message = jsonresp["errors"][0]["message"] @@ -299,9 +296,9 @@ class HacktoberStats(commands.Cog):                  outlist.append(itemdict)          return outlist -    async def _fetch_url(self, url: str, headers: dict) -> dict: +    async def _fetch_url(self, url: str, headers: dict, params: dict) -> dict:          """Retrieve API response from URL.""" -        async with self.bot.http_session.get(url, headers=headers) as resp: +        async with self.bot.http_session.get(url, headers=headers, params=params) as resp:              return await resp.json()      @staticmethod diff --git a/bot/exts/halloween/scarymovie.py b/bot/exts/halloween/scarymovie.py index f4cf41db..33659fd8 100644 --- a/bot/exts/halloween/scarymovie.py +++ b/bot/exts/halloween/scarymovie.py @@ -1,19 +1,14 @@  import logging  import random -from os import environ  from discord import Embed  from discord.ext import commands  from bot.bot import Bot - +from bot.constants import Tokens  log = logging.getLogger(__name__) -TMDB_API_KEY = environ.get("TMDB_API_KEY") -TMDB_TOKEN = environ.get("TMDB_TOKEN") - -  class ScaryMovie(commands.Cog):      """Selects a random scary movie and embeds info into Discord chat.""" @@ -31,13 +26,14 @@ class ScaryMovie(commands.Cog):      async def select_movie(self) -> dict:          """Selects a random movie and returns a JSON of movie details from TMDb.""" -        url = "https://api.themoviedb.org/4/discover/movie" +        url = "https://api.themoviedb.org/3/discover/movie"          params = { +            "api_key": Tokens.tmdb,              "with_genres": "27", -            "vote_count.gte": "5" +            "vote_count.gte": "5", +            "include_adult": "false"          }          headers = { -            "Authorization": "Bearer " + TMDB_TOKEN,              "Content-Type": "application/json;charset=utf-8"          } @@ -55,7 +51,7 @@ class ScaryMovie(commands.Cog):          # Get full details and credits          async with self.bot.http_session.get(              url=f"https://api.themoviedb.org/3/movie/{selection_id}", -            params={"api_key": TMDB_API_KEY, "append_to_response": "credits"} +            params={"api_key": Tokens.tmdb, "append_to_response": "credits"}          ) as selection:              return await selection.json() diff --git a/bot/exts/pride/pride_leader.py b/bot/exts/pride/pride_leader.py index c3426ad1..8e88183b 100644 --- a/bot/exts/pride/pride_leader.py +++ b/bot/exts/pride/pride_leader.py @@ -6,7 +6,7 @@ from typing import Optional  import discord  from discord.ext import commands -from fuzzywuzzy import fuzz +from rapidfuzz import fuzz  from bot import bot  from bot import constants diff --git a/bot/exts/valentines/lovecalculator.py b/bot/exts/valentines/lovecalculator.py index b10b7bca..1cb10e64 100644 --- a/bot/exts/valentines/lovecalculator.py +++ b/bot/exts/valentines/lovecalculator.py @@ -4,7 +4,7 @@ import json  import logging  import random  from pathlib import Path -from typing import Coroutine, Union +from typing import Coroutine, Optional  import discord  from discord import Member @@ -12,6 +12,8 @@ from discord.ext import commands  from discord.ext.commands import BadArgument, Cog, clean_content  from bot.bot import Bot +from bot.constants import Channels, Client, Lovefest, Month +from bot.utils.decorators import in_month  log = logging.getLogger(__name__) @@ -22,45 +24,45 @@ LOVE_DATA = sorted((int(key), value) for key, value in LOVE_DATA.items())  class LoveCalculator(Cog):      """A cog for calculating the love between two people.""" +    @in_month(Month.FEBRUARY)      @commands.command(aliases=("love_calculator", "love_calc"))      @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) -    async def love(self, ctx: commands.Context, who: Union[Member, str], whom: Union[Member, str] = None) -> None: +    async def love(self, ctx: commands.Context, who: Member, whom: Optional[Member] = None) -> None:          """          Tells you how much the two love each other. -        This command accepts users or arbitrary strings as arguments. -        Users are converted from: +        This command requires at least one member as input, if two are given love will be calculated between +        those two users, if only one is given, the second member is asusmed to be the invoker. +        Members are converted from:            - User ID            - Mention            - name#discrim            - name            - nickname -        Any two arguments will always yield the same result, though the order of arguments matters: -          Running .love joseph erlang will always yield the same result. -          Running .love erlang joseph won't yield the same result as .love joseph erlang - -        If you want to use multiple words for one argument, you must include quotes. -          .love "Zes Vappa" "morning coffee" +        Any two arguments will always yield the same result, regardless of the order of arguments: +          Running .love @joe#6000 @chrisjl#2655 will always yield the same result. +          Running .love @chrisjl#2655 @joe#6000 will yield the same result as before.          """ +        if ( +            Lovefest.role_id not in [role.id for role in who.roles] +            or (whom is not None and Lovefest.role_id not in [role.id for role in whom.roles]) +        ): +            raise BadArgument( +                "This command can only be ran against members with the lovefest role! " +                "This role be can assigned by running " +                f"`{Client.prefix}lovefest sub` in <#{Channels.community_bot_commands}>." +            ) +          if whom is None:              whom = ctx.author -        def normalize(arg: Union[Member, str]) -> Coroutine: -            if isinstance(arg, Member): -                # If we are given a member, return name#discrim without any extra changes -                arg = str(arg) -            else: -                # Otherwise normalise case and remove any leading/trailing whitespace -                arg = arg.strip().title() +        def normalize(arg: Member) -> Coroutine:              # This has to be done manually to be applied to usernames -            return clean_content(escape_markdown=True).convert(ctx, arg) - -        who, whom = [await normalize(arg) for arg in (who, whom)] +            return clean_content(escape_markdown=True).convert(ctx, str(arg)) -        # Make sure user didn't provide something silly such as 10 spaces -        if not (who and whom): -            raise BadArgument("Arguments must be non-empty strings.") +        # Sort to ensure same result for same input, regardless of order +        who, whom = sorted([await normalize(arg) for arg in (who, whom)])          # Hash inputs to guarantee consistent results (hashing algorithm choice arbitrary)          # @@ -87,6 +89,7 @@ class LoveCalculator(Cog):              name="A letter from Dr. Love:",              value=data["text"]          ) +        embed.set_footer(text=f"You can unsubscribe from lovefest by using {Client.prefix}lovefest unsub")          await ctx.send(embed=embed) diff --git a/bot/exts/valentines/movie_generator.py b/bot/exts/valentines/movie_generator.py index 0fc5edb4..d2dc8213 100644 --- a/bot/exts/valentines/movie_generator.py +++ b/bot/exts/valentines/movie_generator.py @@ -1,7 +1,6 @@  import logging  import random  from os import environ -from urllib import parse  import discord  from discord.ext import commands @@ -35,8 +34,8 @@ class RomanceMovieFinder(commands.Cog):              "with_genres": "10749"          }          # The api request url -        request_url = "https://api.themoviedb.org/3/discover/movie?" + parse.urlencode(params) -        async with self.bot.http_session.get(request_url) as resp: +        request_url = "https://api.themoviedb.org/3/discover/movie" +        async with self.bot.http_session.get(request_url, params=params) as resp:              # Trying to load the json file returned from the api              try:                  data = await resp.json() | 
