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.png Binary files differindex 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? |