diff options
Diffstat (limited to 'bot')
| -rw-r--r-- | bot/__init__.py | 65 | ||||
| -rw-r--r-- | bot/__main__.py | 20 | ||||
| -rw-r--r-- | bot/constants.py | 4 | ||||
| -rw-r--r-- | bot/exts/core/extensions.py | 2 | ||||
| -rw-r--r-- | bot/exts/holidays/halloween/candy_collection.py | 7 | ||||
| -rw-r--r-- | bot/exts/utilities/challenges.py | 44 | ||||
| -rw-r--r-- | bot/exts/utilities/conversationstarters.py | 2 | ||||
| -rw-r--r-- | bot/exts/utilities/emoji.py | 2 | ||||
| -rw-r--r-- | bot/exts/utilities/githubinfo.py | 4 | ||||
| -rw-r--r-- | bot/exts/utilities/realpython.py | 16 | ||||
| -rw-r--r-- | bot/exts/utilities/reddit.py | 10 | ||||
| -rw-r--r-- | bot/exts/utilities/wikipedia.py | 6 | ||||
| -rw-r--r-- | bot/exts/utilities/wtf_python.py | 14 | ||||
| -rw-r--r-- | bot/log.py | 99 | ||||
| -rw-r--r-- | bot/monkey_patches.py | 11 | ||||
| -rw-r--r-- | bot/resources/holidays/halloween/bat-clipart.png | bin | 12313 -> 19006 bytes | |||
| -rw-r--r-- | bot/resources/utilities/py_topics.yaml | 1 | 
17 files changed, 188 insertions, 119 deletions
| diff --git a/bot/__init__.py b/bot/__init__.py index cfaee9f8..ae53a5a5 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -7,64 +7,35 @@ except ModuleNotFoundError:  import asyncio  import logging -import logging.handlers  import os  from functools import partial, partialmethod -from pathlib import Path  import arrow +import sentry_sdk  from discord.ext import commands +from sentry_sdk.integrations.logging import LoggingIntegration +from sentry_sdk.integrations.redis import RedisIntegration -from bot import monkey_patches -from bot.constants import Client +from bot import log, monkey_patches -# Configure the "TRACE" logging level (e.g. "log.trace(message)") -logging.TRACE = 5 -logging.addLevelName(logging.TRACE, "TRACE") - -logging.Logger.trace = monkey_patches.trace_log - -# Set timestamp of when execution started (approximately) -start_time = arrow.utcnow() - -# Set up file logging -log_dir = Path("bot/log") -log_file = log_dir / "hackbot.log" -os.makedirs(log_dir, exist_ok=True) - -# File handler rotates logs every 5 MB -file_handler = logging.handlers.RotatingFileHandler( -    log_file, maxBytes=5 * (2**20), backupCount=10, encoding="utf-8", +sentry_logging = LoggingIntegration( +    level=logging.DEBUG, +    event_level=logging.WARNING  ) -file_handler.setLevel(logging.TRACE if Client.debug else logging.DEBUG) -# Console handler prints to terminal -console_handler = logging.StreamHandler() -level = logging.TRACE if Client.debug else logging.INFO -console_handler.setLevel(level) - -# Remove old loggers, if any -root = logging.getLogger() -if root.handlers: -    for handler in root.handlers: -        root.removeHandler(handler) - -# Silence irrelevant loggers -logging.getLogger("discord").setLevel(logging.ERROR) -logging.getLogger("websockets").setLevel(logging.ERROR) -logging.getLogger("PIL").setLevel(logging.ERROR) -logging.getLogger("matplotlib").setLevel(logging.ERROR) -logging.getLogger("async_rediscache").setLevel(logging.WARNING) - -# Setup new logging configuration -logging.basicConfig( -    format="%(asctime)s - %(name)s %(levelname)s: %(message)s", -    datefmt="%D %H:%M:%S", -    level=logging.TRACE if Client.debug else logging.DEBUG, -    handlers=[console_handler, file_handler], +sentry_sdk.init( +    dsn=os.environ.get("BOT_SENTRY_DSN"), +    integrations=[ +        sentry_logging, +        RedisIntegration() +    ], +    release=f"sir-lancebot@{os.environ.get('GIT_SHA', 'foobar')}"  ) -logging.getLogger().info("Logging initialization complete") +log.setup() + +# Set timestamp of when execution started (approximately) +start_time = arrow.utcnow()  # On Windows, the selector event loop is required for aiodns.  if os.name == "nt": diff --git a/bot/__main__.py b/bot/__main__.py index c6e5fa57..6889fe2b 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -1,28 +1,10 @@  import logging -import sentry_sdk -from sentry_sdk.integrations.logging import LoggingIntegration -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.constants import Client, STAFF_ROLES, WHITELISTED_CHANNELS  from bot.utils.decorators import whitelist_check  from bot.utils.extensions import walk_extensions -sentry_logging = LoggingIntegration( -    level=logging.DEBUG, -    event_level=logging.WARNING -) - -sentry_sdk.init( -    dsn=Client.sentry_dsn, -    integrations=[ -        sentry_logging, -        RedisIntegration() -    ], -    release=f"sir-lancebot@{GIT_SHA}" -) -  log = logging.getLogger(__name__)  bot.add_check(whitelist_check(channels=WHITELISTED_CHANNELS, roles=STAFF_ROLES)) diff --git a/bot/constants.py b/bot/constants.py index 0720dd20..2b41b8a4 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -134,11 +134,11 @@ class Client(NamedTuple):      guild = int(environ.get("BOT_GUILD", 267624335836053506))      prefix = environ.get("PREFIX", ".")      token = environ.get("BOT_TOKEN") -    sentry_dsn = environ.get("BOT_SENTRY_DSN")      debug = environ.get("BOT_DEBUG", "true").lower() == "true"      github_bot_repo = "https://github.com/python-discord/sir-lancebot"      # Override seasonal locks: 1 (January) to 12 (December)      month_override = int(environ["MONTH_OVERRIDE"]) if "MONTH_OVERRIDE" in environ else None +    trace_loggers = environ.get("BOT_TRACE_LOGGERS")  class Colours: @@ -347,8 +347,6 @@ WHITELISTED_CHANNELS = (      Channels.voice_chat_1,  ) -GIT_SHA = environ.get("GIT_SHA", "foobar") -  # Bot replies  ERROR_REPLIES = [      "Please don't do that.", diff --git a/bot/exts/core/extensions.py b/bot/exts/core/extensions.py index dbb9e069..d809d2b9 100644 --- a/bot/exts/core/extensions.py +++ b/bot/exts/core/extensions.py @@ -152,7 +152,7 @@ class Extensions(commands.Cog):          Grey indicates that the extension is unloaded.          Green indicates that the extension is currently loaded.          """ -        embed = Embed(colour=Colour.blurple()) +        embed = Embed(colour=Colour.og_blurple())          embed.set_author(              name="Extensions List",              url=Client.github_bot_repo, diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 09bd0e59..bb9c93be 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -83,6 +83,11 @@ class CandyCollection(commands.Cog):          # if its not a candy or skull, and it is one of 10 most recent messages,          # proceed to add a skull/candy with higher chance          if str(reaction.emoji) not in (EMOJIS["SKULL"], EMOJIS["CANDY"]): +            # Ensure the reaction is not for a bot's message so users can't spam +            # reaction buttons like in .help to get candies. +            if message.author.bot: +                return +              recent_message_ids = map(                  lambda m: m.id,                  await self.hacktober_channel.history(limit=10).flatten() @@ -182,7 +187,7 @@ class CandyCollection(commands.Cog):                  for index, record in enumerate(top_five)              ) if top_five else "No Candies" -        e = discord.Embed(colour=discord.Colour.blurple()) +        e = discord.Embed(colour=discord.Colour.og_blurple())          e.add_field(              name="Top Candy Records",              value=generate_leaderboard(), diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index 234eb0be..ab7ae442 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -162,13 +162,20 @@ class Challenges(commands.Cog):              kata_description = "\n".join(kata_description[:1000].split("\n")[:-1]) + "..."              kata_description += f" [continue reading]({kata_url})" +        if kata_information["rank"]["name"] is None: +            embed_color = 8 +            kata_difficulty = "Unable to retrieve difficulty for beta languages." +        else: +            embed_color = int(kata_information["rank"]["name"].replace(" kyu", "")) +            kata_difficulty = kata_information["rank"]["name"] +          kata_embed = Embed(              title=kata_information["name"],              description=kata_description, -            color=MAPPING_OF_KYU[int(kata_information["rank"]["name"].replace(" kyu", ""))], +            color=MAPPING_OF_KYU[embed_color],              url=kata_url          ) -        kata_embed.add_field(name="Difficulty", value=kata_information["rank"]["name"], inline=False) +        kata_embed.add_field(name="Difficulty", value=kata_difficulty, inline=False)          return kata_embed      @staticmethod @@ -268,30 +275,29 @@ class Challenges(commands.Cog):          `.challenge <language> <query>, <difficulty>` - Pulls a random challenge with the query provided,          under that difficulty within the language's scope.          """ -        if language.lower() not in SUPPORTED_LANGUAGES["stable"] + SUPPORTED_LANGUAGES["beta"]: +        language = language.lower() +        if language not in SUPPORTED_LANGUAGES["stable"] + SUPPORTED_LANGUAGES["beta"]:              raise commands.BadArgument("This is not a recognized language on codewars.com!")          get_kata_link = f"https://codewars.com/kata/search/{language}"          params = {} -        if language and not query: -            level = f"-{choice([1, 2, 3, 4, 5, 6, 7, 8])}" -            params["r[]"] = level -        elif "," in query: -            query_splitted = query.split("," if ", " not in query else ", ") +        if query is not None: +            if "," in query: +                query_splitted = query.split("," if ", " not in query else ", ") -            if len(query_splitted) > 2: -                raise commands.BadArgument( -                    "There can only be one comma within the query, separating the difficulty and the query itself." -                ) +                if len(query_splitted) > 2: +                    raise commands.BadArgument( +                        "There can only be one comma within the query, separating the difficulty and the query itself." +                    ) -            query, level = query_splitted -            params["q"] = query -            params["r[]"] = f"-{level}" -        elif query.isnumeric(): -            params["r[]"] = f"-{query}" -        else: -            params["q"] = query +                query, level = query_splitted +                params["q"] = query +                params["r[]"] = f"-{level}" +            elif query.isnumeric(): +                params["r[]"] = f"-{query}" +            else: +                params["q"] = query          params["beta"] = str(language in SUPPORTED_LANGUAGES["beta"]).lower() diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py index dcbfe4d5..8bf2abfd 100644 --- a/bot/exts/utilities/conversationstarters.py +++ b/bot/exts/utilities/conversationstarters.py @@ -53,7 +53,7 @@ class ConvoStarters(commands.Cog):          # No matter what, the form will be shown.          embed = discord.Embed(              description=f"Suggest more topics [here]({SUGGESTION_FORM})!", -            color=discord.Color.blurple() +            color=discord.Colour.og_blurple()          )          try: diff --git a/bot/exts/utilities/emoji.py b/bot/exts/utilities/emoji.py index 83df39cc..fa438d7f 100644 --- a/bot/exts/utilities/emoji.py +++ b/bot/exts/utilities/emoji.py @@ -111,7 +111,7 @@ class Emojis(commands.Cog):                  **Date:** {datetime.strftime(emoji.created_at.replace(tzinfo=None), "%d/%m/%Y")}                  **ID:** {emoji.id}              """), -            color=Color.blurple(), +            color=Color.og_blurple(),              url=str(emoji.url),          ).set_thumbnail(url=emoji.url) diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index d00b408d..539e388b 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -67,7 +67,7 @@ class GithubInfo(commands.Cog):              embed = discord.Embed(                  title=f"`{user_data['login']}`'s GitHub profile info",                  description=f"```\n{user_data['bio']}\n```\n" if user_data["bio"] else "", -                colour=discord.Colour.blurple(), +                colour=discord.Colour.og_blurple(),                  url=user_data["html_url"],                  timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ")              ) @@ -139,7 +139,7 @@ class GithubInfo(commands.Cog):          embed = discord.Embed(              title=repo_data["name"],              description=repo_data["description"], -            colour=discord.Colour.blurple(), +            colour=discord.Colour.og_blurple(),              url=repo_data["html_url"]          ) diff --git a/bot/exts/utilities/realpython.py b/bot/exts/utilities/realpython.py index ef8b2638..bf8f1341 100644 --- a/bot/exts/utilities/realpython.py +++ b/bot/exts/utilities/realpython.py @@ -1,5 +1,6 @@  import logging  from html import unescape +from typing import Optional  from urllib.parse import quote_plus  from discord import Embed @@ -31,9 +32,18 @@ class RealPython(commands.Cog):      @commands.command(aliases=["rp"])      @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) -    async def realpython(self, ctx: commands.Context, *, user_search: str) -> None: -        """Send 5 articles that match the user's search terms.""" -        params = {"q": user_search, "limit": 5, "kind": "article"} +    async def realpython(self, ctx: commands.Context, amount: Optional[int] = 5, *, user_search: str) -> None: +        """ +        Send some articles from RealPython that match the search terms. + +        By default the top 5 matches are sent, this can be overwritten to +        a number between 1 and 5 by specifying an amount before the search query. +        """ +        if not 1 <= amount <= 5: +            await ctx.send("`amount` must be between 1 and 5 (inclusive).") +            return + +        params = {"q": user_search, "limit": amount, "kind": "article"}          async with self.bot.http_session.get(url=API_ROOT, params=params) as response:              if response.status != 200:                  logger.error( diff --git a/bot/exts/utilities/reddit.py b/bot/exts/utilities/reddit.py index e6cb5337..782583d2 100644 --- a/bot/exts/utilities/reddit.py +++ b/bot/exts/utilities/reddit.py @@ -244,7 +244,7 @@ class Reddit(Cog):          # Use only starting summary page for #reddit channel posts.          embed.description = self.build_pagination_pages(posts, paginate=False) -        embed.colour = Colour.blurple() +        embed.colour = Colour.og_blurple()          return embed      @loop() @@ -312,7 +312,7 @@ class Reddit(Cog):          await ctx.send(f"Here are the top {subreddit} posts of all time!")          embed = Embed( -            color=Colour.blurple() +            color=Colour.og_blurple()          )          await ImagePaginator.paginate(pages, ctx, embed) @@ -325,7 +325,7 @@ class Reddit(Cog):          await ctx.send(f"Here are today's top {subreddit} posts!")          embed = Embed( -            color=Colour.blurple() +            color=Colour.og_blurple()          )          await ImagePaginator.paginate(pages, ctx, embed) @@ -338,7 +338,7 @@ class Reddit(Cog):          await ctx.send(f"Here are this week's top {subreddit} posts!")          embed = Embed( -            color=Colour.blurple() +            color=Colour.og_blurple()          )          await ImagePaginator.paginate(pages, ctx, embed) @@ -349,7 +349,7 @@ class Reddit(Cog):          """Send a paginated embed of all the subreddits we're relaying."""          embed = Embed()          embed.title = "Relayed subreddits." -        embed.colour = Colour.blurple() +        embed.colour = Colour.og_blurple()          await LinePaginator.paginate(              RedditConfig.subreddits, diff --git a/bot/exts/utilities/wikipedia.py b/bot/exts/utilities/wikipedia.py index eccc1f8c..e5e8e289 100644 --- a/bot/exts/utilities/wikipedia.py +++ b/bot/exts/utilities/wikipedia.py @@ -82,13 +82,11 @@ class WikipediaSearch(commands.Cog):          if contents:              embed = Embed(                  title="Wikipedia Search Results", -                colour=Color.blurple() +                colour=Color.og_blurple()              )              embed.set_thumbnail(url=WIKI_THUMBNAIL)              embed.timestamp = datetime.utcnow() -            await LinePaginator.paginate( -                contents, ctx, embed -            ) +            await LinePaginator.paginate(contents, ctx, embed, restrict_to_user=ctx.author)          else:              await ctx.send(                  "Sorry, we could not find a wikipedia article using that search term." diff --git a/bot/exts/utilities/wtf_python.py b/bot/exts/utilities/wtf_python.py index 66a022d7..980b3dba 100644 --- a/bot/exts/utilities/wtf_python.py +++ b/bot/exts/utilities/wtf_python.py @@ -79,7 +79,7 @@ class WTFPython(commands.Cog):          return match if certainty > MINIMUM_CERTAINTY else None      @commands.command(aliases=("wtf", "WTF")) -    async def wtf_python(self, ctx: commands.Context, *, query: str) -> None: +    async def wtf_python(self, ctx: commands.Context, *, query: Optional[str] = None) -> None:          """          Search WTF Python repository. @@ -87,6 +87,18 @@ class WTFPython(commands.Cog):          Usage:              --> .wtf wild imports          """ +        if query is None: +            no_query_embed = Embed( +                title="WTF Python?!", +                colour=constants.Colours.dark_green, +                description="A repository filled with suprising snippets that can make you say WTF?!\n\n" +                f"[Go to the Repository]({BASE_URL})" +            ) +            logo = File(LOGO_PATH, filename="wtf_logo.jpg") +            no_query_embed.set_thumbnail(url="attachment://wtf_logo.jpg") +            await ctx.send(embed=no_query_embed, file=logo) +            return +          if len(query) > 50:              embed = Embed(                  title=random.choice(constants.ERROR_REPLIES), diff --git a/bot/log.py b/bot/log.py new file mode 100644 index 00000000..97561be4 --- /dev/null +++ b/bot/log.py @@ -0,0 +1,99 @@ +import logging +import logging.handlers +import os +import sys +from pathlib import Path + +import coloredlogs + +from bot.constants import Client + + +def setup() -> None: +    """Set up loggers.""" +    # Configure the "TRACE" logging level (e.g. "log.trace(message)") +    logging.TRACE = 5 +    logging.addLevelName(logging.TRACE, "TRACE") +    logging.Logger.trace = _monkeypatch_trace + +    format_string = "%(asctime)s | %(name)s | %(levelname)s | %(message)s" +    log_format = logging.Formatter(format_string) +    root_logger = logging.getLogger() + +    # Copied from constants file, which we can't import yet since loggers aren't instantiated +    debug = os.environ.get("BOT_DEBUG", "true").lower() == "true" + +    if debug: +        # Set up file logging +        log_file = Path("logs/sir-lancebot.log") +        log_file.parent.mkdir(exist_ok=True) + +        # File handler rotates logs every 5 MB +        file_handler = logging.handlers.RotatingFileHandler( +            log_file, maxBytes=5 * (2 ** 20), backupCount=10, encoding="utf-8", +        ) +        file_handler.setFormatter(log_format) +        root_logger.addHandler(file_handler) + +    if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: +        coloredlogs.DEFAULT_LEVEL_STYLES = { +            **coloredlogs.DEFAULT_LEVEL_STYLES, +            "trace": {"color": 246}, +            "critical": {"background": "red"}, +            "debug": coloredlogs.DEFAULT_LEVEL_STYLES["info"], +        } + +    if "COLOREDLOGS_LOG_FORMAT" not in os.environ: +        coloredlogs.DEFAULT_LOG_FORMAT = format_string + +    coloredlogs.install(level=logging.TRACE, stream=sys.stdout) + +    root_logger.setLevel(logging.DEBUG if Client.debug else logging.INFO) +    # Silence irrelevant loggers +    logging.getLogger("discord").setLevel(logging.ERROR) +    logging.getLogger("websockets").setLevel(logging.ERROR) +    logging.getLogger("PIL").setLevel(logging.ERROR) +    logging.getLogger("matplotlib").setLevel(logging.ERROR) +    logging.getLogger("async_rediscache").setLevel(logging.WARNING) + +    _set_trace_loggers() + +    root_logger.info("Logging initialization complete") + + +def _monkeypatch_trace(self: logging.Logger, msg: str, *args, **kwargs) -> None: +    """ +    Log 'msg % args' with severity 'TRACE'. + +    To pass exception information, use the keyword argument exc_info with a true value, e.g. +    logger.trace("Houston, we have an %s", "interesting problem", exc_info=1) +    """ +    if self.isEnabledFor(logging.TRACE): +        self._log(logging.TRACE, msg, args, **kwargs) + + +def _set_trace_loggers() -> None: +    """ +    Set loggers to the trace level according to the value from the BOT_TRACE_LOGGERS env var. + +    When the env var is a list of logger names delimited by a comma, +    each of the listed loggers will be set to the trace level. + +    If this list is prefixed with a "!", all of the loggers except the listed ones will be set to the trace level. + +    Otherwise if the env var begins with a "*", +    the root logger is set to the trace level and other contents are ignored. +    """ +    level_filter = Client.trace_loggers +    if level_filter: +        if level_filter.startswith("*"): +            logging.getLogger().setLevel(logging.TRACE) + +        elif level_filter.startswith("!"): +            logging.getLogger().setLevel(logging.TRACE) +            for logger_name in level_filter.strip("!,").split(","): +                logging.getLogger(logger_name).setLevel(logging.DEBUG) + +        else: +            for logger_name in level_filter.strip(",").split(","): +                logging.getLogger(logger_name).setLevel(logging.TRACE) diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py index fe81f2e3..fa6627d1 100644 --- a/bot/monkey_patches.py +++ b/bot/monkey_patches.py @@ -7,17 +7,6 @@ from discord.ext import commands  log = logging.getLogger(__name__) -def trace_log(self: logging.Logger, msg: str, *args, **kwargs) -> None: -    """ -    Log 'msg % args' with severity 'TRACE'. - -    To pass exception information, use the keyword argument exc_info with a true value, e.g. -    logger.trace("Houston, we have an %s", "interesting problem", exc_info=1) -    """ -    if self.isEnabledFor(logging.TRACE): -        self._log(logging.TRACE, msg, args, **kwargs) - -  class Command(commands.Command):      """      A `discord.ext.commands.Command` subclass which supports root aliases. diff --git a/bot/resources/holidays/halloween/bat-clipart.png b/bot/resources/holidays/halloween/bat-clipart.pngBinary files differ index 7df26ba9..fc2f77b0 100644 --- a/bot/resources/holidays/halloween/bat-clipart.png +++ b/bot/resources/holidays/halloween/bat-clipart.png diff --git a/bot/resources/utilities/py_topics.yaml b/bot/resources/utilities/py_topics.yaml index a3fb2ccc..1cd2c325 100644 --- a/bot/resources/utilities/py_topics.yaml +++ b/bot/resources/utilities/py_topics.yaml @@ -33,7 +33,6 @@      - How often do you program in Python?      - How would you learn a new library if needed to do so?      - Have you ever worked with a microcontroller or anything physical with Python before? -    - How good would you say you are at Python so far? Beginner, intermediate, or advanced?      - Have you ever tried making your own programming language?      - Has a recently discovered Python module changed your general use of Python? | 
