Command stats

pull/133/head
stijndcl 2022-09-20 14:47:26 +02:00
parent 7517f844d8
commit 23edc51dbf
4 changed files with 141 additions and 29 deletions

View File

@ -0,0 +1,37 @@
"""Command stats
Revision ID: 3c94051821f8
Revises: b84bb10fb8de
Create Date: 2022-09-20 14:38:41.737628
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "3c94051821f8"
down_revision = "b84bb10fb8de"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"command_stats",
sa.Column("command_stats_id", sa.Integer(), nullable=False),
sa.Column("command", sa.Text(), nullable=False),
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("slash", sa.Boolean(), nullable=False),
sa.Column("context_menu", sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint("command_stats_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("command_stats")
# ### end Alembic commands ###

View File

@ -0,0 +1,38 @@
from datetime import datetime
from typing import Optional, Union
from discord import app_commands
from discord.ext import commands
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import CommandStats
__all__ = ["register_command_invocation"]
CommandT = Union[commands.Command, app_commands.Command, app_commands.ContextMenu]
async def register_command_invocation(
session: AsyncSession, ctx: commands.Context, command: Optional[CommandT], timestamp: datetime
):
"""Create an entry for a command invocation"""
if command is None:
return
# Check the type of invocation
context_menu = isinstance(command, app_commands.ContextMenu)
# (This is a bit uglier but it accounts for hybrid commands)
slash = isinstance(command, app_commands.Command) or (ctx.interaction is not None and not context_menu)
stats = CommandStats(
command=command.qualified_name.lower(),
timestamp=timestamp,
user_id=ctx.author.id,
slash=slash,
context_menu=context_menu,
)
session.add(stats)
await session.commit()

View File

@ -27,6 +27,7 @@ __all__ = [
"Bank", "Bank",
"Birthday", "Birthday",
"Bookmark", "Bookmark",
"CommandStats",
"CustomCommand", "CustomCommand",
"CustomCommandAlias", "CustomCommandAlias",
"DadJoke", "DadJoke",
@ -95,6 +96,18 @@ class Bookmark(Base):
user: User = relationship("User", back_populates="bookmarks", uselist=False, lazy="selectin") user: User = relationship("User", back_populates="bookmarks", uselist=False, lazy="selectin")
class CommandStats(Base):
"""Metrics on how often commands are used"""
__tablename__ = "command_stats"
command_stats_id: int = Column(Integer, primary_key=True)
command: str = Column(Text, nullable=False)
timestamp: datetime = Column(DateTime(timezone=True), nullable=False)
user_id: int = Column(BigInteger, nullable=False)
slash: bool = Column(Boolean, nullable=False)
context_menu: bool = Column(Boolean, nullable=False)
class CustomCommand(Base): class CustomCommand(Base):
"""Custom commands to fill the hole Dyno couldn't""" """Custom commands to fill the hole Dyno couldn't"""

View File

@ -2,6 +2,7 @@ import logging
import os import os
import pathlib import pathlib
from functools import cached_property from functools import cached_property
from typing import Union
import discord import discord
from aiohttp import ClientSession from aiohttp import ClientSession
@ -10,7 +11,7 @@ from discord.ext import commands
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
import settings import settings
from database.crud import custom_commands from database.crud import command_stats, custom_commands
from database.engine import DBSession from database.engine import DBSession
from database.utils.caches import CacheManager from database.utils.caches import CacheManager
from didier.data.embeds.error_embed import create_error_embed from didier.data.embeds.error_embed import create_error_embed
@ -18,6 +19,7 @@ from didier.data.embeds.schedules import Schedule, parse_schedule
from didier.exceptions import HTTPException, NoMatch from didier.exceptions import HTTPException, NoMatch
from didier.utils.discord.prefix import get_prefix from didier.utils.discord.prefix import get_prefix
from didier.utils.easter_eggs import detect_easter_egg from didier.utils.easter_eggs import detect_easter_egg
from didier.utils.types.datetime import tz_aware_now
__all__ = ["Didier"] __all__ = ["Didier"]
@ -194,30 +196,6 @@ class Didier(commands.Bot):
"""Log a warning message""" """Log a warning message"""
await self._log(logging.WARNING, message, log_to_discord) await self._log(logging.WARNING, message, log_to_discord)
async def on_ready(self):
"""Event triggered when the bot is ready"""
print(settings.DISCORD_READY_MESSAGE)
async def on_message(self, message: discord.Message, /) -> None:
"""Event triggered when a message is sent"""
# Ignore messages by bots
if message.author.bot:
return
# Boos react to people that say Dider
if "dider" in message.content.lower() and message.author.id != self.user.id:
await message.add_reaction(settings.DISCORD_BOOS_REACT)
# Potential custom command
if await self._try_invoke_custom_command(message):
return
await self.process_commands(message)
easter_egg = await detect_easter_egg(self, message, self.database_caches.easter_eggs)
if easter_egg is not None:
await message.reply(easter_egg, mention_author=False)
async def _try_invoke_custom_command(self, message: discord.Message) -> bool: async def _try_invoke_custom_command(self, message: discord.Message) -> bool:
"""Check if the message tries to invoke a custom command """Check if the message tries to invoke a custom command
@ -241,9 +219,16 @@ class Didier(commands.Bot):
# Nothing found # Nothing found
return False return False
async def on_thread_create(self, thread: discord.Thread): async def on_app_command_completion(
"""Event triggered when a new thread is created""" self,
await thread.join() interaction: discord.Interaction,
command: Union[discord.app_commands.Command, discord.app_commands.ContextMenu],
):
"""Event triggered when an app command completes successfully"""
ctx = await commands.Context.from_interaction(interaction)
async with self.postgres_session as session:
await command_stats.register_command_invocation(session, ctx, command, tz_aware_now())
async def on_app_command_error(self, interaction: discord.Interaction, exception: AppCommandError): async def on_app_command_error(self, interaction: discord.Interaction, exception: AppCommandError):
"""Event triggered when an application command errors""" """Event triggered when an application command errors"""
@ -257,8 +242,18 @@ class Didier(commands.Bot):
else: else:
return await interaction.followup.send(str(exception.original), ephemeral=True) return await interaction.followup.send(str(exception.original), ephemeral=True)
async def on_command_completion(self, ctx: commands.Context):
"""Event triggered when a message command completes successfully"""
# Hybrid command invocation triggers both this handler and on_app_command_completion
# We handle it in the correct place
if ctx.interaction is not None:
return
async with self.postgres_session as session:
await command_stats.register_command_invocation(session, ctx, ctx.command, tz_aware_now())
async def on_command_error(self, ctx: commands.Context, exception: commands.CommandError, /): async def on_command_error(self, ctx: commands.Context, exception: commands.CommandError, /):
"""Event triggered when a regular command errors""" """Event triggered when a message command errors"""
# If working locally, print everything to your console # If working locally, print everything to your console
if settings.SANDBOX: if settings.SANDBOX:
await super().on_command_error(ctx, exception) await super().on_command_error(ctx, exception)
@ -310,3 +305,32 @@ class Didier(commands.Bot):
embed = create_error_embed(ctx, exception) embed = create_error_embed(ctx, exception)
channel = self.get_channel(settings.ERRORS_CHANNEL) channel = self.get_channel(settings.ERRORS_CHANNEL)
await channel.send(embed=embed) await channel.send(embed=embed)
async def on_message(self, message: discord.Message, /) -> None:
"""Event triggered when a message is sent"""
# Ignore messages by bots
if message.author.bot:
return
# Boos react to people that say Dider
if "dider" in message.content.lower() and message.author.id != self.user.id:
await message.add_reaction(settings.DISCORD_BOOS_REACT)
# Potential custom command
if await self._try_invoke_custom_command(message):
return
await self.process_commands(message)
easter_egg = await detect_easter_egg(self, message, self.database_caches.easter_eggs)
if easter_egg is not None:
await message.reply(easter_egg, mention_author=False)
async def on_ready(self):
"""Event triggered when the bot is ready"""
print(settings.DISCORD_READY_MESSAGE)
async def on_thread_create(self, thread: discord.Thread):
"""Event triggered when a new thread is created"""
# Join threads automatically
await thread.join()