aboutsummaryrefslogtreecommitdiffstats
path: root/bot
diff options
context:
space:
mode:
Diffstat (limited to 'bot')
-rw-r--r--bot/__init__.py65
-rw-r--r--bot/__main__.py20
-rw-r--r--bot/constants.py4
-rw-r--r--bot/exts/core/extensions.py2
-rw-r--r--bot/exts/holidays/halloween/candy_collection.py7
-rw-r--r--bot/exts/utilities/challenges.py44
-rw-r--r--bot/exts/utilities/conversationstarters.py2
-rw-r--r--bot/exts/utilities/emoji.py2
-rw-r--r--bot/exts/utilities/githubinfo.py4
-rw-r--r--bot/exts/utilities/realpython.py16
-rw-r--r--bot/exts/utilities/reddit.py10
-rw-r--r--bot/exts/utilities/wikipedia.py6
-rw-r--r--bot/exts/utilities/wtf_python.py14
-rw-r--r--bot/log.py99
-rw-r--r--bot/monkey_patches.py11
-rw-r--r--bot/resources/holidays/halloween/bat-clipart.pngbin12313 -> 19006 bytes
-rw-r--r--bot/resources/utilities/py_topics.yaml1
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
index 7df26ba9..fc2f77b0 100644
--- a/bot/resources/holidays/halloween/bat-clipart.png
+++ b/bot/resources/holidays/halloween/bat-clipart.png
Binary files differ
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?