Enabled all Ruff linter rules and fixed resulting violations.
This commit is contained in:
@@ -106,7 +106,7 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
host, sep, port_str = args.listen_metrics.rpartition(":")
|
||||
if not sep or not host:
|
||||
parser.error(
|
||||
"--listen-metrics must be in HOST:PORT format (e.g. 127.0.0.1:9090)"
|
||||
"--listen-metrics must be in HOST:PORT format (e.g. 127.0.0.1:9090)",
|
||||
)
|
||||
try:
|
||||
args.listen_metrics = (host, int(port_str))
|
||||
@@ -117,7 +117,7 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser.error(
|
||||
"a Discord bot token is required via --token,"
|
||||
" the TOKEN environment variable,"
|
||||
" or a systemd credential named 'token'"
|
||||
" or a systemd credential named 'token'",
|
||||
)
|
||||
|
||||
return args
|
||||
|
||||
+18
-6
@@ -58,6 +58,7 @@ class Crabstero(commands.Bot):
|
||||
self,
|
||||
token: str,
|
||||
database_path: str,
|
||||
*,
|
||||
ingest_only: bool = False,
|
||||
metrics_address: tuple[str, int] | None = None,
|
||||
) -> None:
|
||||
@@ -104,7 +105,8 @@ class Crabstero(commands.Bot):
|
||||
:raises RuntimeError: If accessed before :meth:`setup_hook` has run.
|
||||
"""
|
||||
if self._db is None:
|
||||
raise RuntimeError("Database is not initialized")
|
||||
msg = "Database is not initialized"
|
||||
raise RuntimeError(msg)
|
||||
return self._db
|
||||
|
||||
@property
|
||||
@@ -123,7 +125,11 @@ class Crabstero(commands.Bot):
|
||||
await server.start()
|
||||
self._metrics_server = server
|
||||
|
||||
from crabstero.listeners import interaction, message, server_events
|
||||
from crabstero.listeners import ( # noqa: PLC0415
|
||||
interaction,
|
||||
message,
|
||||
server_events,
|
||||
)
|
||||
|
||||
if not self._ingest_only:
|
||||
await interaction.setup(self)
|
||||
@@ -133,7 +139,8 @@ class Crabstero(commands.Bot):
|
||||
|
||||
@self.tree.error
|
||||
async def on_app_command_error(
|
||||
interaction: discord.Interaction, error: AppCommandError
|
||||
interaction: discord.Interaction,
|
||||
error: AppCommandError,
|
||||
) -> None:
|
||||
original = (
|
||||
error.original if isinstance(error, CommandInvokeError) else error
|
||||
@@ -203,14 +210,18 @@ class Crabstero(commands.Bot):
|
||||
|
||||
@override
|
||||
async def on_error(
|
||||
self, event_method: str, /, *args: object, **kwargs: object
|
||||
self,
|
||||
event_method: str,
|
||||
/,
|
||||
*args: object,
|
||||
**kwargs: object,
|
||||
) -> None:
|
||||
"""Increment the global error counter for event listener exceptions.
|
||||
|
||||
:param event_method: The name of the event that raised the exception.
|
||||
"""
|
||||
ERRORS.labels(source=event_method).inc()
|
||||
logger.error("Unhandled exception in %s.", event_method, exc_info=True)
|
||||
logger.error("Unhandled exception in %s.", event_method, exc_info=True) # noqa: LOG014
|
||||
|
||||
async def on_ready(self) -> None:
|
||||
"""Log that the bot has started successfully."""
|
||||
@@ -250,7 +261,8 @@ class Crabstero(commands.Bot):
|
||||
if channel.id in self._ingestion_tasks:
|
||||
return
|
||||
task = asyncio.create_task(
|
||||
self._ingest_one(channel), name=f"ingest-{channel.id}"
|
||||
self._ingest_one(channel),
|
||||
name=f"ingest-{channel.id}",
|
||||
)
|
||||
self._ingestion_tasks[channel.id] = task
|
||||
task.add_done_callback(lambda t: self._on_ingestion_done(channel.id, t))
|
||||
|
||||
+2
-1
@@ -103,7 +103,8 @@ class IngestCache:
|
||||
if self._task is not None:
|
||||
return
|
||||
self._task = asyncio.create_task(
|
||||
self._cleanup_loop(), name="ingest-cache-cleanup"
|
||||
self._cleanup_loop(),
|
||||
name="ingest-cache-cleanup",
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
|
||||
+11
-3
@@ -261,7 +261,9 @@ class Database:
|
||||
return row[0] if row else None
|
||||
|
||||
async def get_random_completing_next_word(
|
||||
self, channel_id: int, word: str
|
||||
self,
|
||||
channel_id: int,
|
||||
word: str,
|
||||
) -> str | None:
|
||||
"""Return a random sentence-ending next word for a given word in a channel.
|
||||
|
||||
@@ -352,7 +354,10 @@ class Database:
|
||||
)
|
||||
|
||||
async def clear_flag(
|
||||
self, entity_type: str, entity_id: str, flag_name: str
|
||||
self,
|
||||
entity_type: str,
|
||||
entity_id: str,
|
||||
flag_name: str,
|
||||
) -> None:
|
||||
"""Clear a flag on a given entity. If the flag is not set, this is a no-op.
|
||||
|
||||
@@ -370,7 +375,10 @@ class Database:
|
||||
)
|
||||
|
||||
async def is_flag_set(
|
||||
self, entity_type: str, entity_id: str, flag_name: str
|
||||
self,
|
||||
entity_type: str,
|
||||
entity_id: str,
|
||||
flag_name: str,
|
||||
) -> bool:
|
||||
"""Check whether a flag is set on a given entity.
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ class ForgetMeView(TrackedView):
|
||||
"""Confirmation view with Confirm/Cancel buttons for /forgetme."""
|
||||
|
||||
def __init__(
|
||||
self, user_id: int, db: Database, interaction: discord.Interaction
|
||||
self,
|
||||
user_id: int,
|
||||
db: Database,
|
||||
interaction: discord.Interaction,
|
||||
) -> None:
|
||||
"""Create a new ForgetMeView.
|
||||
|
||||
@@ -64,12 +67,14 @@ class ForgetMeView(TrackedView):
|
||||
|
||||
@discord.ui.button(label="Confirm", style=discord.ButtonStyle.danger)
|
||||
async def confirm(
|
||||
self, interaction: discord.Interaction, button: discord.ui.Button[ForgetMeView]
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
_button: discord.ui.Button[ForgetMeView],
|
||||
) -> None:
|
||||
"""Delete all user data, clear flags, and set noIngest.
|
||||
|
||||
:param interaction: The interaction event.
|
||||
:param button: The button that was pressed.
|
||||
:param _button: The button that was pressed.
|
||||
"""
|
||||
self._responded = True
|
||||
await self._db.forget_user(self._user_id, Flag.NO_INGEST)
|
||||
@@ -85,12 +90,14 @@ class ForgetMeView(TrackedView):
|
||||
|
||||
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary)
|
||||
async def cancel(
|
||||
self, interaction: discord.Interaction, button: discord.ui.Button[ForgetMeView]
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
_button: discord.ui.Button[ForgetMeView],
|
||||
) -> None:
|
||||
"""Cancel the /forgetme action.
|
||||
|
||||
:param interaction: The interaction event.
|
||||
:param button: The button that was pressed.
|
||||
:param _button: The button that was pressed.
|
||||
"""
|
||||
self._responded = True
|
||||
metrics.FORGETME.labels(outcome="cancelled").inc()
|
||||
@@ -145,10 +152,16 @@ class InteractionCog(commands.Cog):
|
||||
:param interaction: The interaction event.
|
||||
"""
|
||||
if await flags.is_flag_set(
|
||||
self.bot.db, interaction.user, EntityType.USER, Flag.ALLOW_PINGS
|
||||
self.bot.db,
|
||||
interaction.user,
|
||||
EntityType.USER,
|
||||
Flag.ALLOW_PINGS,
|
||||
):
|
||||
await flags.clear_flag(
|
||||
self.bot.db, interaction.user, EntityType.USER, Flag.ALLOW_PINGS
|
||||
self.bot.db,
|
||||
interaction.user,
|
||||
EntityType.USER,
|
||||
Flag.ALLOW_PINGS,
|
||||
)
|
||||
metrics.PINGME.labels(outcome="opted_out").inc()
|
||||
await interaction.response.send_message(
|
||||
@@ -159,7 +172,10 @@ class InteractionCog(commands.Cog):
|
||||
)
|
||||
else:
|
||||
await flags.set_flag(
|
||||
self.bot.db, interaction.user, EntityType.USER, Flag.ALLOW_PINGS
|
||||
self.bot.db,
|
||||
interaction.user,
|
||||
EntityType.USER,
|
||||
Flag.ALLOW_PINGS,
|
||||
)
|
||||
metrics.PINGME.labels(outcome="opted_in").inc()
|
||||
await interaction.response.send_message(
|
||||
@@ -179,7 +195,10 @@ class InteractionCog(commands.Cog):
|
||||
:param interaction: The interaction event.
|
||||
"""
|
||||
if await flags.is_flag_set(
|
||||
self.bot.db, interaction.user, EntityType.USER, Flag.NO_INGEST
|
||||
self.bot.db,
|
||||
interaction.user,
|
||||
EntityType.USER,
|
||||
Flag.NO_INGEST,
|
||||
):
|
||||
metrics.FORGETME.labels(outcome="already_forgotten").inc()
|
||||
await interaction.response.send_message(
|
||||
|
||||
@@ -49,7 +49,8 @@ class MessageCog(commands.Cog):
|
||||
channel = message.channel
|
||||
|
||||
if not isinstance(
|
||||
channel, (discord.TextChannel, discord.Thread, discord.VoiceChannel)
|
||||
channel,
|
||||
(discord.TextChannel, discord.Thread, discord.VoiceChannel),
|
||||
):
|
||||
return
|
||||
|
||||
@@ -61,7 +62,8 @@ class MessageCog(commands.Cog):
|
||||
if self.bot.ingest_only:
|
||||
# In ingest-only mode, never reply — only ingest eligible messages.
|
||||
if message.type == discord.MessageType.default and not isinstance(
|
||||
channel, discord.Thread
|
||||
channel,
|
||||
discord.Thread,
|
||||
):
|
||||
await ingest_message(self.bot.db, message, self.bot.ingest_cache)
|
||||
return
|
||||
@@ -72,13 +74,15 @@ class MessageCog(commands.Cog):
|
||||
|
||||
# Only ingest non-thread messages; threads share their parent channel's chain.
|
||||
if message.type == discord.MessageType.default and not isinstance(
|
||||
channel, discord.Thread
|
||||
channel,
|
||||
discord.Thread,
|
||||
):
|
||||
await ingest_message(self.bot.db, message, self.bot.ingest_cache)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_raw_message_delete(
|
||||
self, payload: discord.RawMessageDeleteEvent
|
||||
self,
|
||||
payload: discord.RawMessageDeleteEvent,
|
||||
) -> None:
|
||||
"""Uningest a recently ingested message when it is deleted.
|
||||
|
||||
@@ -88,7 +92,8 @@ class MessageCog(commands.Cog):
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_raw_bulk_message_delete(
|
||||
self, payload: discord.RawBulkMessageDeleteEvent
|
||||
self,
|
||||
payload: discord.RawBulkMessageDeleteEvent,
|
||||
) -> None:
|
||||
"""Uningest recently ingested messages when they are bulk-deleted.
|
||||
|
||||
|
||||
@@ -55,7 +55,8 @@ class ServerEventsCog(commands.Cog):
|
||||
logger.info('Joined new server! (Name: "%s" | ID: %s)', guild.name, guild.id)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Joined New Server", color=discord.Color.from_rgb(255, 165, 0)
|
||||
title="Joined New Server",
|
||||
color=discord.Color.from_rgb(255, 165, 0),
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"{guild.name} ({guild.id})",
|
||||
@@ -84,7 +85,9 @@ class ServerEventsCog(commands.Cog):
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_role_update(
|
||||
self, before: discord.Role, after: discord.Role
|
||||
self,
|
||||
before: discord.Role,
|
||||
after: discord.Role,
|
||||
) -> None:
|
||||
"""Queue all text channels for ingestion when role permissions change.
|
||||
|
||||
@@ -101,7 +104,9 @@ class ServerEventsCog(commands.Cog):
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_channel_update(
|
||||
self, before: discord.abc.GuildChannel, after: discord.abc.GuildChannel
|
||||
self,
|
||||
before: discord.abc.GuildChannel,
|
||||
after: discord.abc.GuildChannel,
|
||||
) -> None:
|
||||
"""Queue all text channels for ingestion on permission change.
|
||||
|
||||
|
||||
+16
-4
@@ -73,6 +73,9 @@ def _split_sentences(paragraph: str) -> list[str]:
|
||||
return _SENTENCE_SPLIT.split(normalized)
|
||||
|
||||
|
||||
_MIN_WORDS_FOR_START = 2
|
||||
|
||||
|
||||
def _tokenize_sentence(
|
||||
sentence: str,
|
||||
) -> tuple[list[str], list[tuple[str, str]]]:
|
||||
@@ -85,7 +88,7 @@ def _tokenize_sentence(
|
||||
sentence += DEFAULT_SENTENCE_END
|
||||
words = sentence.split()
|
||||
|
||||
start_words: list[str] = [words[0]] if len(words) >= 2 else []
|
||||
start_words: list[str] = [words[0]] if len(words) >= _MIN_WORDS_FOR_START else []
|
||||
transitions: list[tuple[str, str]] = list(itertools.pairwise(words))
|
||||
return start_words, transitions
|
||||
|
||||
@@ -106,7 +109,10 @@ async def ingest(db: Database, channel_id: int, user_id: int, paragraph: str) ->
|
||||
|
||||
|
||||
async def _ingest_sentence(
|
||||
db: Database, channel_id: int, user_id: int, sentence: str
|
||||
db: Database,
|
||||
channel_id: int,
|
||||
user_id: int,
|
||||
sentence: str,
|
||||
) -> None:
|
||||
"""Ingest a single sentence into the Markov chain for a given channel.
|
||||
|
||||
@@ -137,7 +143,10 @@ async def uningest(db: Database, channel_id: int, user_id: int, paragraph: str)
|
||||
|
||||
|
||||
async def _uningest_sentence(
|
||||
db: Database, channel_id: int, user_id: int, sentence: str
|
||||
db: Database,
|
||||
channel_id: int,
|
||||
user_id: int,
|
||||
sentence: str,
|
||||
) -> None:
|
||||
"""Remove a single sentence's Markov data from the chain.
|
||||
|
||||
@@ -153,7 +162,10 @@ async def _uningest_sentence(
|
||||
|
||||
|
||||
async def generate(
|
||||
db: Database, channel_id: int, soft_limit: int = 750, hard_limit: int = 1000
|
||||
db: Database,
|
||||
channel_id: int,
|
||||
soft_limit: int = 750,
|
||||
hard_limit: int = 1000,
|
||||
) -> str:
|
||||
"""Generate a new sentence from previously ingested words for a channel.
|
||||
|
||||
|
||||
+32
-15
@@ -79,7 +79,10 @@ async def reply_to_message(db: Database, message: discord.Message) -> None:
|
||||
embed = discord.Embed(
|
||||
title=await markov.generate(db, channel_id, soft_limit=200, hard_limit=300),
|
||||
description=await markov.generate(
|
||||
db, channel_id, soft_limit=300, hard_limit=500
|
||||
db,
|
||||
channel_id,
|
||||
soft_limit=300,
|
||||
hard_limit=500,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -117,8 +120,30 @@ async def reply_to_message(db: Database, message: discord.Message) -> None:
|
||||
metrics.REPLIES_SENT.inc()
|
||||
|
||||
|
||||
def _extract_embed_data(
|
||||
embeds: list[discord.Embed],
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""Extract text and image URLs from message embeds.
|
||||
|
||||
:param embeds: The embeds to process.
|
||||
:return: A tuple of (texts, image_urls).
|
||||
"""
|
||||
texts: list[str] = []
|
||||
image_urls: list[str] = []
|
||||
for embed in embeds:
|
||||
if embed.title:
|
||||
texts.append(embed.title)
|
||||
if embed.description:
|
||||
texts.append(embed.description)
|
||||
if embed.image and embed.image.url:
|
||||
image_urls.append(embed.image.url)
|
||||
return texts, image_urls
|
||||
|
||||
|
||||
async def ingest_message(
|
||||
db: Database, message: discord.Message, cache: IngestCache | None = None
|
||||
db: Database,
|
||||
message: discord.Message,
|
||||
cache: IngestCache | None = None,
|
||||
) -> None:
|
||||
"""Ingest a given message into its channel's Markov chain.
|
||||
|
||||
@@ -150,26 +175,18 @@ async def ingest_message(
|
||||
if not message.content and not message.embeds:
|
||||
return
|
||||
|
||||
embed_texts: list[str] = []
|
||||
image_urls: list[str] = []
|
||||
embed_texts, image_urls = _extract_embed_data(message.embeds)
|
||||
|
||||
with metrics.MESSAGE_INGESTION_DURATION.time():
|
||||
if message.content:
|
||||
await markov.ingest(db, channel_id, user_id, message.content)
|
||||
|
||||
for embed in message.embeds:
|
||||
if embed.title:
|
||||
embed_texts.append(embed.title)
|
||||
await markov.ingest(db, channel_id, user_id, embed.title)
|
||||
if embed.description:
|
||||
embed_texts.append(embed.description)
|
||||
await markov.ingest(db, channel_id, user_id, embed.description)
|
||||
if embed.image and embed.image.url:
|
||||
image_urls.append(embed.image.url)
|
||||
for text in embed_texts:
|
||||
await markov.ingest(db, channel_id, user_id, text)
|
||||
|
||||
if image_urls:
|
||||
await db.add_images(
|
||||
[ChannelImage(channel_id, user_id, url) for url in image_urls]
|
||||
[ChannelImage(channel_id, user_id, url) for url in image_urls],
|
||||
)
|
||||
|
||||
if cache is not None:
|
||||
@@ -210,7 +227,7 @@ async def uningest_message(db: Database, cache: IngestCache, message_id: int) ->
|
||||
[
|
||||
ChannelImage(entry.channel_id, entry.user_id, url)
|
||||
for url in entry.image_urls
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
metrics.MESSAGES_UNINGESTED.inc()
|
||||
|
||||
@@ -148,7 +148,8 @@ class MetricsServer:
|
||||
async def start(self) -> None:
|
||||
"""Create the aiohttp application and start listening."""
|
||||
if self._runner is not None:
|
||||
raise RuntimeError("Metrics server is already running")
|
||||
msg = "Metrics server is already running"
|
||||
raise RuntimeError(msg)
|
||||
app = web.Application()
|
||||
app.router.add_get("/metrics", make_aiohttp_handler())
|
||||
self._runner = web.AppRunner(app)
|
||||
|
||||
Reference in New Issue
Block a user