From ecfe6bc1b89632c1161f5aad83a08a8459f8f13a Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Sep 2021 00:32:12 +0200 Subject: Add selective trace loggers Add selective enabling of trace loggers as described in python-discord/bot#1529 --- bot/constants.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bot/constants.py') diff --git a/bot/constants.py b/bot/constants.py index 2313bfdb..ae6e02c1 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -138,6 +138,7 @@ class Client(NamedTuple): 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: -- cgit v1.2.3 From b0be25bed15bbfc88d61e2e842b9f894c9cac15c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 13 Oct 2021 20:44:07 +0100 Subject: update advent of code channel IDs We deleted and re-made the channels so new IDs are needed. --- bot/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot/constants.py') diff --git a/bot/constants.py b/bot/constants.py index 6e45632f..567daadd 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -101,8 +101,8 @@ class Cats: class Channels(NamedTuple): - advent_of_code = int(environ.get("AOC_CHANNEL_ID", 782715290437943306)) - advent_of_code_commands = int(environ.get("AOC_COMMANDS_CHANNEL_ID", 607247579608121354)) + advent_of_code = int(environ.get("AOC_CHANNEL_ID", 897932085766004786)) + advent_of_code_commands = int(environ.get("AOC_COMMANDS_CHANNEL_ID", 897932607545823342)) bot = 267659945086812160 organisation = 551789653284356126 devlog = int(environ.get("CHANNEL_DEVLOG", 622895325144940554)) -- cgit v1.2.3 From f18e9c3dda721b9bbbba884e31b34e4ae2831ffc Mon Sep 17 00:00:00 2001 From: D0rs4n <41237606+D0rs4n@users.noreply.github.com> Date: Thu, 14 Oct 2021 21:55:41 +0200 Subject: Add support to query AoC results in respect of days and stars (#857) * Add support to query AoC results in respect of days and stars From now on the AoC leaderboard command accepts a total of 2 optional arguments a day and star string (eg.: 1-2, for the second star of the first day) and a number of results they would like to see, with a total maximum of 15. This commit also introduces a few minor fixes in the AoC helper. * Improve overall code consitency in the AoC event Cog and helpers * Improve indenting and code consistency in the AoC cog * Improve code transparency in the AoC helpers * Patch various inconsistencies in the AoC cog and helpers * Migrate AoC Day and Star statistics filtering to Dropdowns From now on when the AoC leadearboard command is used with the DayAndStar argument(bool) the bot will send a View with two dropdowns and a button to Fetch the data based on the value of the Dropdowns. * Improve code and comment consistency in the AoC views and helpers * Patch logic errors, improve consistency in the AoC cog and view. * Add support to delete view from the message after timeout in the AoC cog * Move the day_and_star logic out of the typing context manager in the AoC cog * Revert season-locker in the AoC cog * Improve overall code transparency and indenting in the AoC cog and views * Remove unnecessary returns in the AoC cog and view --- bot/constants.py | 1 + bot/exts/events/advent_of_code/_cog.py | 45 ++++++++++++-- bot/exts/events/advent_of_code/_helpers.py | 9 ++- .../events/advent_of_code/views/dayandstarview.py | 71 ++++++++++++++++++++++ 4 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 bot/exts/events/advent_of_code/views/dayandstarview.py (limited to 'bot/constants.py') diff --git a/bot/constants.py b/bot/constants.py index 567daadd..0720dd20 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -88,6 +88,7 @@ class AdventOfCode: ignored_days = environ.get("AOC_IGNORED_DAYS", "").split(",") leaderboard_displayed_members = 10 leaderboard_cache_expiry_seconds = 1800 + max_day_and_star_results = 15 year = int(environ.get("AOC_YEAR", datetime.utcnow().year)) role_id = int(environ.get("AOC_ROLE_ID", 518565788744024082)) diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index ca60e517..7dd967ec 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -2,6 +2,7 @@ import json import logging from datetime import datetime, timedelta from pathlib import Path +from typing import Optional import arrow import discord @@ -12,6 +13,7 @@ from bot.constants import ( AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS, ) from bot.exts.events.advent_of_code import _helpers +from bot.exts.events.advent_of_code.views.dayandstarview import AoCDropdownView from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role from bot.utils.extensions import invoke_help_command @@ -150,7 +152,7 @@ class AdventOfCode(commands.Cog): else: try: join_code = await _helpers.get_public_join_code(author) - except _helpers.FetchingLeaderboardFailed: + except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Failed to get join code! Notified maintainers.") return @@ -185,14 +187,29 @@ class AdventOfCode(commands.Cog): brief="Get a snapshot of the PyDis private AoC leaderboard", ) @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 def aoc_leaderboard( + self, + ctx: commands.Context, + day_and_star: Optional[bool] = False, + maximum_scorers: Optional[int] = 10 + ) -> None: + """ + Get the current top scorers of the Python Discord Leaderboard. + + Additionally, you can provide an argument `day_and_star` (Boolean) to have the bot send a View + that will let you filter by day and star. + """ + if maximum_scorers > AocConfig.max_day_and_star_results or maximum_scorers <= 0: + raise commands.BadArgument( + f"The maximum number of results you can query is {AocConfig.max_day_and_star_results}" + ) async with ctx.typing(): try: leaderboard = await _helpers.fetch_leaderboard() - except _helpers.FetchingLeaderboardFailed: + except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Unable to fetch leaderboard!") return + if not day_and_star: number_of_participants = leaderboard["number_of_participants"] @@ -203,6 +220,22 @@ class AdventOfCode(commands.Cog): info_embed = _helpers.get_summary_embed(leaderboard) await ctx.send(content=f"{header}\n\n{table}", embed=info_embed) + return + + # This is a dictionary that contains solvers in respect of day, and star. + # e.g. 1-1 means the solvers of the first star of the first day and their completion time + per_day_and_star = json.loads(leaderboard['leaderboard_per_day_and_star']) + view = AoCDropdownView( + day_and_star_data=per_day_and_star, + maximum_scorers=maximum_scorers, + original_author=ctx.author + ) + message = await ctx.send( + content="Please select a day and a star to filter by!", + view=view + ) + await view.wait() + await message.edit(view=None) @in_month(Month.DECEMBER) @adventofcode_group.command( @@ -231,7 +264,7 @@ class AdventOfCode(commands.Cog): """Send an embed with daily completion statistics for the Python Discord leaderboard.""" try: leaderboard = await _helpers.fetch_leaderboard() - except _helpers.FetchingLeaderboardFailed: + except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Can't fetch leaderboard for stats right now!") return @@ -267,7 +300,7 @@ class AdventOfCode(commands.Cog): async with ctx.typing(): try: await _helpers.fetch_leaderboard(invalidate_cache=True) - except _helpers.FetchingLeaderboardFailed: + except _helpers.FetchingLeaderboardFailedError: await ctx.send(":x: Something went wrong while trying to refresh the cache!") else: await ctx.send("\N{OK Hand Sign} Refreshed leaderboard cache!") diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index 5fedb60f..af64bc81 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -105,6 +105,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: # The data we get from the AoC website is structured by member, not by day/star, # which means we need to iterate over the members to transpose the data to a per # star view. We need that per star view to compute rank scores per star. + per_day_star_stats = collections.defaultdict(list) for member in raw_leaderboard_data.values(): name = member["name"] if member["name"] else f"Anonymous #{member['id']}" member_id = member["id"] @@ -122,6 +123,11 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: star_results[(day, star)].append( StarResult(member_id=member_id, completion_time=completion_time) ) + per_day_star_stats[f"{day}-{star}"].append( + {'completion_time': int(data["get_star_ts"]), 'member_name': name} + ) + for key in per_day_star_stats: + per_day_star_stats[key] = sorted(per_day_star_stats[key], key=operator.itemgetter('completion_time')) # Now that we have a transposed dataset that holds the completion time of all # participants per star, we can compute the rank-based scores each participant @@ -151,7 +157,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict: # this data to JSON in order to cache it in Redis. daily_stats[day] = {"star_one": star_one, "star_two": star_two} - return {"daily_stats": daily_stats, "leaderboard": sorted_leaderboard} + return {"daily_stats": daily_stats, "leaderboard": sorted_leaderboard, 'per_day_and_star': per_day_star_stats} def _format_leaderboard(leaderboard: dict[str, dict]) -> str: @@ -289,6 +295,7 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict: "leaderboard_fetched_at": leaderboard_fetched_at, "number_of_participants": number_of_participants, "daily_stats": json.dumps(parsed_leaderboard_data["daily_stats"]), + "leaderboard_per_day_and_star": json.dumps(parsed_leaderboard_data["per_day_and_star"]) } # Store the new values in Redis diff --git a/bot/exts/events/advent_of_code/views/dayandstarview.py b/bot/exts/events/advent_of_code/views/dayandstarview.py new file mode 100644 index 00000000..243db32e --- /dev/null +++ b/bot/exts/events/advent_of_code/views/dayandstarview.py @@ -0,0 +1,71 @@ +from datetime import datetime + +import discord + +AOC_DAY_AND_STAR_TEMPLATE = "{rank: >4} | {name:25.25} | {completion_time: >10}" + + +class AoCDropdownView(discord.ui.View): + """Interactive view to filter AoC stats by Day and Star.""" + + def __init__(self, original_author: discord.Member, day_and_star_data: dict[str: dict], maximum_scorers: int): + super().__init__() + self.day = 0 + self.star = 0 + self.data = day_and_star_data + self.maximum_scorers = maximum_scorers + self.original_author = original_author + + def generate_output(self) -> str: + """Generates a formatted codeblock with AoC statistics based on the currently selected day and star.""" + header = AOC_DAY_AND_STAR_TEMPLATE.format( + rank="Rank", + name="Name", completion_time="Completion time (UTC)" + ) + lines = [f"{header}\n{'-' * (len(header) + 2)}"] + + for rank, scorer in enumerate(self.data[f"{self.day}-{self.star}"][:self.maximum_scorers]): + time_data = datetime.fromtimestamp(scorer['completion_time']).strftime("%I:%M:%S %p") + lines.append(AOC_DAY_AND_STAR_TEMPLATE.format( + datastamp="", + rank=rank + 1, + name=scorer['member_name'], + completion_time=time_data) + ) + joined_lines = "\n".join(lines) + return f"Statistics for Day: {self.day}, Star: {self.star}.\n ```\n{joined_lines}\n```" + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + """Global check to ensure that the interacting user is the user who invoked the command originally.""" + return interaction.user == self.original_author + + @discord.ui.select( + placeholder="Day", + options=[discord.SelectOption(label=str(i)) for i in range(1, 26)], + custom_id="day_select" + ) + async def day_select(self, select: discord.ui.Select, interaction: discord.Interaction) -> None: + """Dropdown to choose a Day of the AoC.""" + self.day = select.values[0] + + @discord.ui.select( + placeholder="Star", + options=[discord.SelectOption(label=str(i)) for i in range(1, 3)], + custom_id="star_select" + ) + async def star_select(self, select: discord.ui.Select, interaction: discord.Interaction) -> None: + """Dropdown to choose either the first or the second star.""" + self.star = select.values[0] + + @discord.ui.button(label="Fetch", style=discord.ButtonStyle.blurple) + async def fetch(self, button: discord.ui.Button, interaction: discord.Interaction) -> None: + """Button that fetches the statistics based on the dropdown values.""" + if self.day == 0 or self.star == 0: + await interaction.response.send_message( + "You have to select a value from both of the dropdowns!", + ephemeral=True + ) + else: + await interaction.response.edit_message(content=self.generate_output()) + self.day = 0 + self.star = 0 -- cgit v1.2.3 From 40b58132d488bab962d8aeecfc932a1cb5842a85 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 12 Nov 2021 06:19:59 +0400 Subject: Move Sentry Into Init Moves the sentry setup to be one of the very first things run during startup, so we are able to catch more errors, such as ones that might occur while setting up logs. Signed-off-by: Hassan Abouelela --- bot/__init__.py | 18 ++++++++++++++++++ bot/__main__.py | 20 +------------------- bot/constants.py | 3 --- 3 files changed, 19 insertions(+), 22 deletions(-) (limited to 'bot/constants.py') diff --git a/bot/__init__.py b/bot/__init__.py index b19bd76a..ae53a5a5 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -6,14 +6,32 @@ except ModuleNotFoundError: pass import asyncio +import logging import os from functools import partial, partialmethod 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 log, monkey_patches +sentry_logging = LoggingIntegration( + level=logging.DEBUG, + event_level=logging.WARNING +) + +sentry_sdk.init( + dsn=os.environ.get("BOT_SENTRY_DSN"), + integrations=[ + sentry_logging, + RedisIntegration() + ], + release=f"sir-lancebot@{os.environ.get('GIT_SHA', 'foobar')}" +) + log.setup() # Set timestamp of when execution started (approximately) 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 9d12000e..2b41b8a4 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -134,7 +134,6 @@ 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) @@ -348,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.", -- cgit v1.2.3 From b794b02c4aa81218500151fde53e95d8c10c9817 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Sat, 20 Nov 2021 23:10:57 +0300 Subject: Disable File Logging By Default Place logging to file behind an environment variable. Signed-off-by: Hassan Abouelela --- bot/constants.py | 1 + bot/log.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) (limited to 'bot/constants.py') diff --git a/bot/constants.py b/bot/constants.py index 2b41b8a4..33bc8b61 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -135,6 +135,7 @@ class Client(NamedTuple): prefix = environ.get("PREFIX", ".") token = environ.get("BOT_TOKEN") debug = environ.get("BOT_DEBUG", "true").lower() == "true" + file_logs = environ.get("FILE_LOGS", "false").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 diff --git a/bot/log.py b/bot/log.py index 97561be4..29e696e0 100644 --- a/bot/log.py +++ b/bot/log.py @@ -20,10 +20,7 @@ def setup() -> None: 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: + if Client.file_logs: # Set up file logging log_file = Path("logs/sir-lancebot.log") log_file.parent.mkdir(exist_ok=True) -- cgit v1.2.3