mirror of https://github.com/stijndcl/didier
Command stats
parent
7517f844d8
commit
23edc51dbf
|
@ -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 ###
|
|
@ -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()
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue