diff options
Diffstat (limited to '')
| -rw-r--r-- | Pipfile.lock | 99 | ||||
| -rw-r--r-- | bot/__main__.py | 5 | ||||
| -rw-r--r-- | bot/constants.py | 9 | ||||
| -rw-r--r-- | bot/exts/christmas/advent_of_code/_cog.py | 20 | ||||
| -rw-r--r-- | bot/exts/evergreen/cheatsheet.py | 107 | ||||
| -rw-r--r-- | bot/exts/evergreen/conversationstarters.py | 4 | ||||
| -rw-r--r-- | bot/exts/evergreen/status_cats.py | 33 | ||||
| -rw-r--r-- | bot/exts/evergreen/status_codes.py | 71 | ||||
| -rw-r--r-- | bot/exts/evergreen/wolfram.py | 11 | ||||
| -rw-r--r-- | bot/exts/evergreen/xkcd.py | 2 | ||||
| -rw-r--r-- | bot/exts/halloween/hacktoberstats.py | 8 | ||||
| -rw-r--r-- | bot/exts/valentines/be_my_valentine.py | 80 | ||||
| -rw-r--r-- | bot/exts/valentines/lovecalculator.py | 11 | ||||
| -rw-r--r-- | bot/utils/decorators.py | 119 | ||||
| -rw-r--r-- | docker-compose.yml | 8 | 
15 files changed, 353 insertions, 234 deletions
| diff --git a/Pipfile.lock b/Pipfile.lock index cca89cf9..bd894ffa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "b4aaaacbab13179145e36d7b86c736db512286f6cce8e513cc30c48d68fe3810" +            "sha256": "9be419062bd9db364ac9dddfcd50aef9c932384b45850363e482591fe7d12403"          },          "pipfile-spec": 6,          "requires": { @@ -103,45 +103,45 @@          },          "cffi": {              "hashes": [ -                "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", -                "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", -                "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", -                "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", -                "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", -                "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", -                "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", -                "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", -                "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", -                "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", -                "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", -                "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", -                "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", -                "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", -                "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", -                "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", -                "sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e", -                "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", -                "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", -                "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", -                "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", -                "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", -                "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", -                "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", -                "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", -                "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", -                "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", -                "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", -                "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", -                "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", -                "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", -                "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", -                "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", -                "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", -                "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", -                "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", -                "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" -            ], -            "version": "==1.14.4" +                "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", +                "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", +                "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", +                "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", +                "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", +                "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", +                "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", +                "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", +                "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", +                "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", +                "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", +                "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", +                "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", +                "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", +                "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", +                "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", +                "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", +                "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", +                "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", +                "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", +                "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", +                "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", +                "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", +                "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", +                "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", +                "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", +                "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", +                "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", +                "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", +                "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", +                "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", +                "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", +                "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", +                "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", +                "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", +                "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", +                "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" +            ], +            "version": "==1.14.5"          },          "chardet": {              "hashes": [ @@ -161,14 +161,6 @@              "index": "pypi",              "version": "==1.5.1"          }, -        "emojis": { -            "hashes": [ -                "sha256:7da34c8a78ae262fd68cef9e2c78a3c1feb59784489eeea0f54ba1d4b7111c7c", -                "sha256:bf605d1f1a27a81cd37fe82eb65781c904467f569295a541c33710b97e4225ec" -            ], -            "index": "pypi", -            "version": "==0.6.0" -        },          "fakeredis": {              "hashes": [                  "sha256:01cb47d2286825a171fb49c0e445b1fa9307087e07cbb3d027ea10dbff108b6a", @@ -414,11 +406,10 @@          },          "sentry-sdk": {              "hashes": [ -                "sha256:0a711ec952441c2ec89b8f5d226c33bc697914f46e876b44a4edd3e7864cf4d0", -                "sha256:737a094e49a529dd0fdcaafa9e97cf7c3d5eb964bd229821d640bc77f3502b3f" +                "sha256:3693cb47ba8d90c004ac002425770b32aaf0c83a846ec48e2d1364e7db1d072d"              ],              "index": "pypi", -            "version": "==0.19.5" +            "version": "==0.20.1"          },          "six": {              "hashes": [ @@ -437,11 +428,11 @@          },          "soupsieve": {              "hashes": [ -                "sha256:4bb21a6ee4707bf43b61230e80740e71bfe56e55d1f1f50924b087bb2975c851", -                "sha256:6dc52924dc0bc710a5d16794e6b3480b2c7c08b07729505feab2b2c16661ff6e" +                "sha256:407fa1e8eb3458d1b5614df51d9651a1180ea5fedf07feb46e45d7e25e6d6cdd", +                "sha256:d3a5ea5b350423f47d07639f74475afedad48cf41c0ad7a82ca13a3928af34f6"              ],              "markers": "python_version >= '3.0'", -            "version": "==2.1" +            "version": "==2.2"          },          "urllib3": {              "hashes": [ diff --git a/bot/__main__.py b/bot/__main__.py index e9b14a53..c6e5fa57 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -6,10 +6,9 @@ from sentry_sdk.integrations.redis import RedisIntegration  from bot.bot import bot  from bot.constants import Client, GIT_SHA, STAFF_ROLES, WHITELISTED_CHANNELS -from bot.utils.decorators import in_channel_check +from bot.utils.decorators import whitelist_check  from bot.utils.extensions import walk_extensions -  sentry_logging = LoggingIntegration(      level=logging.DEBUG,      event_level=logging.WARNING @@ -26,7 +25,7 @@ sentry_sdk.init(  log = logging.getLogger(__name__) -bot.add_check(in_channel_check(*WHITELISTED_CHANNELS, bypass_roles=STAFF_ROLES)) +bot.add_check(whitelist_check(channels=WHITELISTED_CHANNELS, roles=STAFF_ROLES))  for ext in walk_extensions():      bot.load_extension(ext) diff --git a/bot/constants.py b/bot/constants.py index 1234ef3b..bb538487 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -106,12 +106,6 @@ class Channels(NamedTuple):      devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554))      dev_contrib = 635950537262759947      dev_branding = 753252897059373066 -    help_0 = 303906576991780866 -    help_1 = 303906556754395136 -    help_2 = 303906514266226689 -    help_3 = 439702951246692352 -    help_4 = 451312046647148554 -    help_5 = 454941769734422538      helpers = 385474242440986624      message_log = 467752170159079424      mod_alerts = 473092532147060736 @@ -133,6 +127,7 @@ class Channels(NamedTuple):  class Categories(NamedTuple): +    help_in_use = 696958401460043776      development = 411199786025484308      devprojects = 787641585624940544      media = 799054581991997460 @@ -248,7 +243,6 @@ class Roles(NamedTuple):      announcements = 463658397560995840      champion = 430492892331769857      contributor = 295488872404484098 -    developer = 352427296948486144      devops = 409416496733880320      jammer = 423054537079783434      moderator = 267629731250176001 @@ -259,6 +253,7 @@ class Roles(NamedTuple):      rockstars = 458226413825294336      core_developers = 587606783669829632      events_lead = 778361735739998228 +    everyone_role = 267624335836053506  class Tokens(NamedTuple): diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py index c3b87f96..466edd48 100644 --- a/bot/exts/christmas/advent_of_code/_cog.py +++ b/bot/exts/christmas/advent_of_code/_cog.py @@ -11,7 +11,7 @@ from bot.constants import (      AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS,  )  from bot.exts.christmas.advent_of_code import _helpers -from bot.utils.decorators import InChannelCheckFailure, in_month, override_in_channel, with_role +from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role  log = logging.getLogger(__name__) @@ -50,7 +50,7 @@ class AdventOfCode(commands.Cog):          self.status_task.add_done_callback(_helpers.background_task_callback)      @commands.group(name="adventofcode", aliases=("aoc",)) -    @override_in_channel(AOC_WHITELIST) +    @whitelist_override(channels=AOC_WHITELIST)      async def adventofcode_group(self, ctx: commands.Context) -> None:          """All of the Advent of Code commands."""          if not ctx.invoked_subcommand: @@ -61,7 +61,7 @@ class AdventOfCode(commands.Cog):          aliases=("sub", "notifications", "notify", "notifs"),          brief="Notifications for new days"      ) -    @override_in_channel(AOC_WHITELIST) +    @whitelist_override(channels=AOC_WHITELIST)      async def aoc_subscribe(self, ctx: commands.Context) -> None:          """Assign the role for notifications about new days being ready."""          current_year = datetime.now().year @@ -82,7 +82,7 @@ class AdventOfCode(commands.Cog):      @in_month(Month.DECEMBER)      @adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days") -    @override_in_channel(AOC_WHITELIST) +    @whitelist_override(channels=AOC_WHITELIST)      async def aoc_unsubscribe(self, ctx: commands.Context) -> None:          """Remove the role for notifications about new days being ready."""          role = ctx.guild.get_role(AocConfig.role_id) @@ -94,7 +94,7 @@ class AdventOfCode(commands.Cog):              await ctx.send("Hey, you don't even get any notifications about new Advent of Code tasks currently anyway.")      @adventofcode_group.command(name="countdown", aliases=("count", "c"), brief="Return time left until next day") -    @override_in_channel(AOC_WHITELIST) +    @whitelist_override(channels=AOC_WHITELIST)      async def aoc_countdown(self, ctx: commands.Context) -> None:          """Return time left until next day."""          if not _helpers.is_in_advent(): @@ -123,13 +123,13 @@ class AdventOfCode(commands.Cog):          await ctx.send(f"There are {hours} hours and {minutes} minutes left until day {tomorrow.day}.")      @adventofcode_group.command(name="about", aliases=("ab", "info"), brief="Learn about Advent of Code") -    @override_in_channel(AOC_WHITELIST) +    @whitelist_override(channels=AOC_WHITELIST)      async def about_aoc(self, ctx: commands.Context) -> None:          """Respond with an explanation of all things Advent of Code."""          await ctx.send("", embed=self.cached_about_aoc)      @adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join the leaderboard (via DM)") -    @override_in_channel(AOC_WHITELIST) +    @whitelist_override(channels=AOC_WHITELIST)      async def join_leaderboard(self, ctx: commands.Context) -> None:          """DM the user the information for joining the Python Discord leaderboard."""          current_year = datetime.now().year @@ -178,7 +178,7 @@ class AdventOfCode(commands.Cog):          aliases=("board", "lb"),          brief="Get a snapshot of the PyDis private AoC leaderboard",      ) -    @override_in_channel(AOC_WHITELIST_RESTRICTED) +    @whitelist_override(channels=AOC_WHITELIST_RESTRICTED)      async def aoc_leaderboard(self, ctx: commands.Context) -> None:          """Get the current top scorers of the Python Discord Leaderboard."""          async with ctx.typing(): @@ -203,7 +203,7 @@ class AdventOfCode(commands.Cog):          aliases=("globalboard", "gb"),          brief="Get a link to the global leaderboard",      ) -    @override_in_channel(AOC_WHITELIST_RESTRICTED) +    @whitelist_override(channels=AOC_WHITELIST_RESTRICTED)      async def aoc_global_leaderboard(self, ctx: commands.Context) -> None:          """Get a link to the global Advent of Code leaderboard."""          url = self.global_leaderboard_url @@ -219,7 +219,7 @@ class AdventOfCode(commands.Cog):          aliases=("dailystats", "ds"),          brief="Get daily statistics for the Python Discord leaderboard"      ) -    @override_in_channel(AOC_WHITELIST_RESTRICTED) +    @whitelist_override(channels=AOC_WHITELIST_RESTRICTED)      async def private_leaderboard_daily_stats(self, ctx: commands.Context) -> None:          """Send an embed with daily completion statistics for the Python Discord leaderboard."""          try: diff --git a/bot/exts/evergreen/cheatsheet.py b/bot/exts/evergreen/cheatsheet.py new file mode 100644 index 00000000..3fe709d5 --- /dev/null +++ b/bot/exts/evergreen/cheatsheet.py @@ -0,0 +1,107 @@ +import random +import re +import typing as t +from urllib.parse import quote_plus + +from discord import Embed +from discord.ext import commands +from discord.ext.commands import BucketType, Context + +from bot import constants +from bot.constants import Categories, Channels, Colours, ERROR_REPLIES +from bot.utils.decorators import whitelist_override + +ERROR_MESSAGE = f""" +Unknown cheat sheet. Please try to reformulate your query. + +**Examples**: +```md +{constants.Client.prefix}cht read json +{constants.Client.prefix}cht hello world +{constants.Client.prefix}cht lambda +``` +If the problem persists send a message in <#{Channels.dev_contrib}> +""" + +URL = 'https://cheat.sh/python/{search}' +ESCAPE_TT = str.maketrans({"`": "\\`"}) +ANSI_RE = re.compile(r"\x1b\[.*?m") +# We need to pass headers as curl otherwise it would default to aiohttp which would return raw html. +HEADERS = {'User-Agent': 'curl/7.68.0'} + + +class CheatSheet(commands.Cog): +    """Commands that sends a result of a cht.sh search in code blocks.""" + +    def __init__(self, bot: commands.Bot): +        self.bot = bot + +    @staticmethod +    def fmt_error_embed() -> Embed: +        """ +        Format the Error Embed. + +        If the cht.sh search returned 404, overwrite it to send a custom error embed. +        link -> https://github.com/chubin/cheat.sh/issues/198 +        """ +        embed = Embed( +            title=random.choice(ERROR_REPLIES), +            description=ERROR_MESSAGE, +            colour=Colours.soft_red +        ) +        return embed + +    def result_fmt(self, url: str, body_text: str) -> t.Tuple[bool, t.Union[str, Embed]]: +        """Format Result.""" +        if body_text.startswith("#  404 NOT FOUND"): +            embed = self.fmt_error_embed() +            return True, embed + +        body_space = min(1986 - len(url), 1000) + +        if len(body_text) > body_space: +            description = (f"**Result Of cht.sh**\n" +                           f"```python\n{body_text[:body_space]}\n" +                           f"... (truncated - too many lines)```\n" +                           f"Full results: {url} ") +        else: +            description = (f"**Result Of cht.sh**\n" +                           f"```python\n{body_text}```\n" +                           f"{url}") +        return False, description + +    @commands.command( +        name="cheat", +        aliases=("cht.sh", "cheatsheet", "cheat-sheet", "cht"), +    ) +    @commands.cooldown(1, 10, BucketType.user) +    @whitelist_override(categories=[Categories.help_in_use]) +    async def cheat_sheet(self, ctx: Context, *search_terms: str) -> None: +        """ +        Search cheat.sh. + +        Gets a post from https://cheat.sh/python/ by default. +        Usage: +        --> .cht read json +        """ +        async with ctx.typing(): +            search_string = quote_plus(" ".join(search_terms)) + +            async with self.bot.http_session.get( +                    URL.format(search=search_string), headers=HEADERS +            ) as response: +                result = ANSI_RE.sub("", await response.text()).translate(ESCAPE_TT) + +            is_embed, description = self.result_fmt( +                URL.format(search=search_string), +                result +            ) +            if is_embed: +                await ctx.send(embed=description) +            else: +                await ctx.send(content=description) + + +def setup(bot: commands.Bot) -> None: +    """Load the CheatSheet cog.""" +    bot.add_cog(CheatSheet(bot)) diff --git a/bot/exts/evergreen/conversationstarters.py b/bot/exts/evergreen/conversationstarters.py index 576b8d76..e7058961 100644 --- a/bot/exts/evergreen/conversationstarters.py +++ b/bot/exts/evergreen/conversationstarters.py @@ -5,7 +5,7 @@ from discord import Color, Embed  from discord.ext import commands  from bot.constants import WHITELISTED_CHANNELS -from bot.utils.decorators import override_in_channel +from bot.utils.decorators import whitelist_override  from bot.utils.randomization import RandomCycle  SUGGESTION_FORM = 'https://forms.gle/zw6kkJqv8U43Nfjg9' @@ -38,7 +38,7 @@ class ConvoStarters(commands.Cog):          self.bot = bot      @commands.command() -    @override_in_channel(ALL_ALLOWED_CHANNELS) +    @whitelist_override(channels=ALL_ALLOWED_CHANNELS)      async def topic(self, ctx: commands.Context) -> None:          """          Responds with a random topic to start a conversation. diff --git a/bot/exts/evergreen/status_cats.py b/bot/exts/evergreen/status_cats.py deleted file mode 100644 index 586b8378..00000000 --- a/bot/exts/evergreen/status_cats.py +++ /dev/null @@ -1,33 +0,0 @@ -from http import HTTPStatus - -import discord -from discord.ext import commands - - -class StatusCats(commands.Cog): -    """Commands that give HTTP statuses described and visualized by cats.""" - -    def __init__(self, bot: commands.Bot): -        self.bot = bot - -    @commands.command(aliases=['statuscat']) -    async def http_cat(self, ctx: commands.Context, code: int) -> None: -        """Sends an embed with an image of a cat, potraying the status code.""" -        embed = discord.Embed(title=f'**Status: {code}**') - -        try: -            HTTPStatus(code) - -        except ValueError: -            embed.set_footer(text='Inputted status code does not exist.') - -        else: -            embed.set_image(url=f'https://http.cat/{code}.jpg') - -        finally: -            await ctx.send(embed=embed) - - -def setup(bot: commands.Bot) -> None: -    """Load the StatusCats cog.""" -    bot.add_cog(StatusCats(bot)) diff --git a/bot/exts/evergreen/status_codes.py b/bot/exts/evergreen/status_codes.py new file mode 100644 index 00000000..874c87eb --- /dev/null +++ b/bot/exts/evergreen/status_codes.py @@ -0,0 +1,71 @@ +from http import HTTPStatus + +import discord +from discord.ext import commands + +HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg" +HTTP_CAT_URL = "https://http.cat/{code}.jpg" + + +class HTTPStatusCodes(commands.Cog): +    """Commands that give HTTP statuses described and visualized by cats and dogs.""" + +    def __init__(self, bot: commands.Bot): +        self.bot = bot + +    @commands.group(name="http_status", aliases=("status", "httpstatus")) +    async def http_status_group(self, ctx: commands.Context) -> None: +        """Group containing dog and cat http status code commands.""" +        if not ctx.invoked_subcommand: +            await ctx.send_help(ctx.command) + +    @http_status_group.command(name='cat') +    async def http_cat(self, ctx: commands.Context, code: int) -> None: +        """Sends an embed with an image of a cat, portraying the status code.""" +        embed = discord.Embed(title=f'**Status: {code}**') +        url = HTTP_CAT_URL.format(code=code) + +        try: +            HTTPStatus(code) +            async with self.bot.http_session.get(url, allow_redirects=False) as response: +                if response.status != 404: +                    embed.set_image(url=url) +                else: +                    raise NotImplementedError + +        except ValueError: +            embed.set_footer(text='Inputted status code does not exist.') + +        except NotImplementedError: +            embed.set_footer(text='Inputted status code is not implemented by http.cat yet.') + +        finally: +            await ctx.send(embed=embed) + +    @http_status_group.command(name='dog') +    async def http_dog(self, ctx: commands.Context, code: int) -> None: +        """Sends an embed with an image of a dog, portraying the status code.""" +        embed = discord.Embed(title=f'**Status: {code}**') +        url = HTTP_DOG_URL.format(code=code) + +        try: +            HTTPStatus(code) +            async with self.bot.http_session.get(url, allow_redirects=False) as response: +                if response.status != 302: +                    embed.set_image(url=url) +                else: +                    raise NotImplementedError + +        except ValueError: +            embed.set_footer(text='Inputted status code does not exist.') + +        except NotImplementedError: +            embed.set_footer(text='Inputted status code is not implemented by httpstatusdogs.com yet.') + +        finally: +            await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: +    """Load the HTTPStatusCodes cog.""" +    bot.add_cog(HTTPStatusCodes(bot)) diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py index 898e8d2a..437d9e1a 100644 --- a/bot/exts/evergreen/wolfram.py +++ b/bot/exts/evergreen/wolfram.py @@ -108,7 +108,10 @@ async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional              "input": query,              "appid": APPID,              "output": DEFAULT_OUTPUT_FORMAT, -            "format": "image,plaintext" +            "format": "image,plaintext", +            "location": "the moon", +            "latlong": "0.0,0.0", +            "ip": "1.1.1.1"          })          request_url = QUERY.format(request="query", data=url_str) @@ -168,6 +171,9 @@ class Wolfram(Cog):          url_str = parse.urlencode({              "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) @@ -248,6 +254,9 @@ class Wolfram(Cog):          url_str = parse.urlencode({              "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) diff --git a/bot/exts/evergreen/xkcd.py b/bot/exts/evergreen/xkcd.py index d3224bfe..1ff98ca2 100644 --- a/bot/exts/evergreen/xkcd.py +++ b/bot/exts/evergreen/xkcd.py @@ -69,6 +69,8 @@ class XKCD(Cog):                      return          embed.title = f"XKCD comic #{info['num']}" +        embed.description = info['alt'] +        embed.url = f"{BASE_URL}/{info['num']}"          if info["img"][-3:] in ("jpg", "png", "gif"):              embed.set_image(url=info["img"]) diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py index a1c55922..d9fc0e8a 100644 --- a/bot/exts/halloween/hacktoberstats.py +++ b/bot/exts/halloween/hacktoberstats.py @@ -11,7 +11,7 @@ from async_rediscache import RedisCache  from discord.ext import commands  from bot.constants import Channels, Month, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS -from bot.utils.decorators import in_month, override_in_channel +from bot.utils.decorators import in_month, whitelist_override  log = logging.getLogger(__name__) @@ -44,7 +44,7 @@ class HacktoberStats(commands.Cog):      @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True) -    @override_in_channel(HACKTOBER_WHITELIST) +    @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 +72,7 @@ class HacktoberStats(commands.Cog):      @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @hacktoberstats_group.command(name="link") -    @override_in_channel(HACKTOBER_WHITELIST) +    @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 +96,7 @@ class HacktoberStats(commands.Cog):      @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @hacktoberstats_group.command(name="unlink") -    @override_in_channel(HACKTOBER_WHITELIST) +    @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) diff --git a/bot/exts/valentines/be_my_valentine.py b/bot/exts/valentines/be_my_valentine.py index 4db4d191..f3392bcb 100644 --- a/bot/exts/valentines/be_my_valentine.py +++ b/bot/exts/valentines/be_my_valentine.py @@ -2,13 +2,13 @@ import logging  import random  from json import load  from pathlib import Path -from typing import Optional, Tuple +from typing import Tuple  import discord  from discord.ext import commands  from discord.ext.commands.cooldowns import BucketType -from bot.constants import Channels, Client, Colours, Lovefest, Month +from bot.constants import Channels, Colours, Lovefest, Month  from bot.utils.decorators import in_month  log = logging.getLogger(__name__) @@ -70,44 +70,35 @@ class BeMyValentine(commands.Cog):      @commands.cooldown(1, 1800, BucketType.user)      @commands.group(name='bemyvalentine', invoke_without_command=True)      async def send_valentine( -        self, ctx: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None +        self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None      ) -> None:          """ -        Send a valentine to user, if specified, or to a random user with the lovefest role. +        Send a valentine to a specified user with the lovefest role. -        syntax: .bemyvalentine [user](optional) [p/poem/c/compliment/or you can type your own valentine message] +        syntax: .bemyvalentine [user] [p/poem/c/compliment/or you can type your own valentine message]          (optional) -        example: .bemyvalentine (sends valentine as a poem or a compliment to a random user)          example: .bemyvalentine Iceman#6508 p (sends a poem to Iceman)          example: .bemyvalentine Iceman Hey I love you, wanna hang around ? (sends the custom message to Iceman)          NOTE : AVOID TAGGING THE USER MOST OF THE TIMES.JUST TRIM THE '@' when using this command.          """          if ctx.guild is None:              # This command should only be used in the server -            msg = "You are supposed to use this command in the server." -            return await ctx.send(msg) +            raise commands.UserInputError("You are supposed to use this command in the server.") -        if user: -            if Lovefest.role_id not in [role.id for role in user.roles]: -                message = f"You cannot send a valentine to {user} as he/she does not have the lovefest role!" -                return await ctx.send(message) +        if Lovefest.role_id not in [role.id for role in user.roles]: +            raise commands.UserInputError( +                f"You cannot send a valentine to {user} as they do not have the lovefest role!" +            )          if user == ctx.author:              # Well a user can't valentine himself/herself. -            return await ctx.send("Come on dude, you can't send a valentine to yourself :expressionless:") +            raise commands.UserInputError("Come on, you can't send a valentine to yourself :expressionless:")          emoji_1, emoji_2 = self.random_emoji() -        lovefest_role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)          channel = self.bot.get_channel(Channels.community_bot_commands)          valentine, title = self.valentine_check(valentine_type) -        if user is None: -            author = ctx.author -            user = self.random_user(author, lovefest_role.members) -            if user is None: -                return await ctx.send("There are no users avilable to whome your valentine can be sent.") -          embed = discord.Embed(              title=f'{emoji_1} {title} {user.display_name} {emoji_2}',              description=f'{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**', @@ -118,56 +109,41 @@ class BeMyValentine(commands.Cog):      @commands.cooldown(1, 1800, BucketType.user)      @send_valentine.command(name='secret')      async def anonymous( -        self, ctx: commands.Context, user: Optional[discord.Member] = None, *, valentine_type: str = None +        self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None      ) -> None:          """ -        Send an anonymous Valentine via DM to to a user, if specified, or to a random with the lovefest role. - -        **This command should be DMed to the bot.** +        Send an anonymous Valentine via DM to to a specified user with the lovefest role. -        syntax : .bemyvalentine secret [user](optional) [p/poem/c/compliment/or you can type your own valentine message] +        syntax : .bemyvalentine secret [user] [p/poem/c/compliment/or you can type your own valentine message]          (optional) -        example : .bemyvalentine secret (sends valentine as a poem or a compliment to a random user in DM making you -        anonymous)          example : .bemyvalentine secret Iceman#6508 p (sends a poem to Iceman in DM making you anonymous)          example : .bemyvalentine secret Iceman#6508 Hey I love you, wanna hang around ? (sends the custom message to          Iceman in DM making you anonymous)          """ -        if ctx.guild is not None: -            # This command is only DM specific -            msg = "You are not supposed to use this command in the server, DM the command to the bot." -            return await ctx.send(msg) - -        if user: -            if Lovefest.role_id not in [role.id for role in user.roles]: -                message = f"You cannot send a valentine to {user} as he/she does not have the lovefest role!" -                return await ctx.send(message) +        if Lovefest.role_id not in [role.id for role in user.roles]: +            await ctx.message.delete() +            raise commands.UserInputError( +                f"You cannot send a valentine to {user} as they do not have the lovefest role!" +            )          if user == ctx.author:              # Well a user cant valentine himself/herself. -            return await ctx.send('Come on dude, you cant send a valentine to yourself :expressionless:') +            raise commands.UserInputError("Come on, you can't send a valentine to yourself :expressionless:") -        guild = self.bot.get_guild(id=Client.guild)          emoji_1, emoji_2 = self.random_emoji() -        lovefest_role = discord.utils.get(guild.roles, id=Lovefest.role_id)          valentine, title = self.valentine_check(valentine_type) -        if user is None: -            author = ctx.author -            user = self.random_user(author, lovefest_role.members) -            if user is None: -                return await ctx.send("There are no users avilable to whome your valentine can be sent.") -          embed = discord.Embed(              title=f'{emoji_1}{title} {user.display_name}{emoji_2}',              description=f'{valentine} \n **{emoji_2}From anonymous{emoji_1}**',              color=Colours.pink          ) +        await ctx.message.delete()          try:              await user.send(embed=embed)          except discord.Forbidden: -            await ctx.author.send(f"{user} has DMs disabled, so I couldn't send the message. Sorry!") +            raise commands.UserInputError(f"{user} has DMs disabled, so I couldn't send the message. Sorry!")          else:              await ctx.author.send(f"Your message has been sent to {user}") @@ -191,18 +167,6 @@ class BeMyValentine(commands.Cog):          return valentine, title      @staticmethod -    def random_user(author: discord.Member, members: discord.Member) -> None: -        """ -        Picks a random member from the list provided in `members`. - -        The invoking author is ignored. -        """ -        if author in members: -            members.remove(author) - -        return random.choice(members) if members else None - -    @staticmethod      def random_emoji() -> Tuple[str, str]:          """Return two random emoji from the module-defined constants."""          emoji_1 = random.choice(HEART_EMOJIS) diff --git a/bot/exts/valentines/lovecalculator.py b/bot/exts/valentines/lovecalculator.py index c75ea6cf..966acc82 100644 --- a/bot/exts/valentines/lovecalculator.py +++ b/bot/exts/valentines/lovecalculator.py @@ -4,15 +4,13 @@ import json  import logging  import random  from pathlib import Path -from typing import Union +from typing import Coroutine, Union  import discord  from discord import Member  from discord.ext import commands  from discord.ext.commands import BadArgument, Cog, clean_content -from bot.constants import Roles -  log = logging.getLogger(__name__)  with Path("bot/resources/valentines/love_matches.json").open(encoding="utf8") as file: @@ -46,14 +44,11 @@ class LoveCalculator(Cog):          If you want to use multiple words for one argument, you must include quotes.            .love "Zes Vappa" "morning coffee" - -        If only one argument is provided, the subject will become one of the helpers at random.          """          if whom is None: -            staff = ctx.guild.get_role(Roles.helpers).members -            whom = random.choice(staff) +            whom = ctx.author -        def normalize(arg: Union[Member, str]) -> str: +        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) diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py index 9cdaad3f..c12a15ff 100644 --- a/bot/utils/decorators.py +++ b/bot/utils/decorators.py @@ -13,6 +13,7 @@ from discord.ext.commands import CheckFailure, Command, Context  from bot.constants import ERROR_REPLIES, Month  from bot.utils import human_months, resolve_current_month +from bot.utils.checks import in_whitelist_check  ONE_DAY = 24 * 60 * 60 @@ -186,82 +187,104 @@ def without_role(*role_ids: int) -> t.Callable:      return commands.check(predicate) -def in_channel_check(*channels: int, bypass_roles: t.Container[int] = None) -> t.Callable[[Context], bool]: +def whitelist_check(**default_kwargs: t.Container[int]) -> t.Callable[[Context], bool]:      """ -    Checks that the message is in a whitelisted channel or optionally has a bypass role. +    Checks if a message is sent in a whitelisted context. -    If `in_channel_override` is present, check if it contains channels -    and use them in place of the global whitelist. +    All arguments from `in_whitelist_check` are supported, with the exception of "fail_silently". +    If `whitelist_override` is present, it is added to the global whitelist.      """      def predicate(ctx: Context) -> bool: +        # Skip DM invocations          if not ctx.guild:              log.debug(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM.")              return True -        if ctx.channel.id in channels: -            log.debug( -                f"{ctx.author} tried to call the '{ctx.command.name}' command " -                f"and the command was used in a whitelisted channel." -            ) -            return True -        if bypass_roles and any(r.id in bypass_roles for r in ctx.author.roles): -            log.debug( -                f"{ctx.author} called the '{ctx.command.name}' command and " -                f"had a role to bypass the in_channel check." -            ) -            return True +        kwargs = default_kwargs.copy() -        if hasattr(ctx.command.callback, "in_channel_override"): -            override = ctx.command.callback.in_channel_override -            if override is None: +        # Update kwargs based on override +        if hasattr(ctx.command.callback, "override"): +            # Remove default kwargs if reset is True +            if ctx.command.callback.override_reset: +                kwargs = {}                  log.debug( -                    f"{ctx.author} called the '{ctx.command.name}' command " -                    f"and the command was whitelisted to bypass the in_channel check." +                    f"{ctx.author} called the '{ctx.command.name}' command and " +                    f"overrode default checks."                  ) -                return True -            else: -                if ctx.channel.id in override: -                    log.debug( -                        f"{ctx.author} tried to call the '{ctx.command.name}' command " -                        f"and the command was used in an overridden whitelisted channel." -                    ) -                    return True -                log.debug( -                    f"{ctx.author} tried to call the '{ctx.command.name}' command. " -                    f"The overridden in_channel check failed." -                ) -                channels_str = ', '.join(f"<#{c_id}>" for c_id in override) -                raise InChannelCheckFailure( -                    f"Sorry, but you may only use this command within {channels_str}." -                ) +            # Merge overwrites and defaults +            for arg in ctx.command.callback.override: +                default_value = kwargs.get(arg) +                new_value = ctx.command.callback.override[arg] + +                # Skip values that don't need merging, or can't be merged +                if default_value is None or isinstance(arg, int): +                    kwargs[arg] = new_value + +                # Merge containers +                elif isinstance(default_value, t.Container): +                    if isinstance(new_value, t.Container): +                        kwargs[arg] = (*default_value, *new_value) +                    else: +                        kwargs[arg] = new_value + +            log.debug( +                f"Updated default check arguments for '{ctx.command.name}' " +                f"invoked by {ctx.author}." +            ) + +        log.trace(f"Calling whitelist check for {ctx.author} for command {ctx.command.name}.") +        result = in_whitelist_check(ctx, fail_silently=True, **kwargs) + +        # Return if check passed +        if result: +            log.debug( +                f"{ctx.author} tried to call the '{ctx.command.name}' command " +                f"and the command was used in an overridden context." +            ) +            return result          log.debug(              f"{ctx.author} tried to call the '{ctx.command.name}' command. " -            f"The in_channel check failed." +            f"The whitelist check failed."          ) -        channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) -        raise InChannelCheckFailure( -            f"Sorry, but you may only use this command within {channels_str}." -        ) +        # Raise error if the check did not pass +        channels = set(kwargs.get("channels") or {}) +        categories = kwargs.get("categories") -    return predicate +        # Add all whitelisted category channels +        if categories: +            for category_id in categories: +                category = ctx.guild.get_channel(category_id) +                if category is None: +                    continue +                [channels.add(channel.id) for channel in category.text_channels] -in_channel = commands.check(in_channel_check) +        if channels: +            channels_str = ', '.join(f"<#{c_id}>" for c_id in channels) +            message = f"Sorry, but you may only use this command within {channels_str}." +        else: +            message = "Sorry, but you may not use this command." + +        raise InChannelCheckFailure(message) + +    return predicate -def override_in_channel(channels: t.Tuple[int] = None) -> t.Callable: +def whitelist_override(bypass_defaults: bool = False, **kwargs: t.Container[int]) -> t.Callable:      """ -    Set command callback attribute for detection in `in_channel_check`. +    Override global whitelist context, with the kwargs specified. -    Override global whitelist if channels are specified. +    All arguments from `in_whitelist_check` are supported, with the exception of `fail_silently`. +    Set `bypass_defaults` to True if you want to completely bypass global checks.      This decorator has to go before (below) below the `command` decorator.      """      def inner(func: t.Callable) -> t.Callable: -        func.in_channel_override = channels +        func.override = kwargs +        func.override_reset = bypass_defaults          return func      return inner diff --git a/docker-compose.yml b/docker-compose.yml index bb6ad6ac..a18534a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,13 +12,9 @@ services:        - redis      environment: -      - BOT_TOKEN -      - BOT_DEBUG -      - BOT_GUILD -      - BOT_ADMIN_ROLE_ID -      - CHANNEL_DEVLOG -      - CHANNEL_COMMUNITY_BOT_COMMANDS        - REDIS_HOST=redis +    env_file: +      - .env      volumes:        - .:/bot | 
