Removed several commands
This commit is contained in:
288
cogs/misc.py
288
cogs/misc.py
@@ -158,23 +158,6 @@ class Misc(commands.Cog):
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Error: {str(e)}")
|
||||
|
||||
@bridge.bridge_command() # type: ignore
|
||||
@is_spamchan_or_drugs()
|
||||
async def listshoves(self, ctx) -> None:
|
||||
"""
|
||||
List Available Fates for shove command
|
||||
"""
|
||||
fates: str = ""
|
||||
try:
|
||||
for fate in self.FATES:
|
||||
fates += f"**- {fate}**\n"
|
||||
embed: discord.Embed = discord.Embed(
|
||||
title="Available Fates (for .shove)", description=fates.strip()
|
||||
)
|
||||
return await ctx.respond(embed=embed)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Error: {str(e)}")
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def xmas(self, ctx) -> None:
|
||||
@@ -277,46 +260,6 @@ class Misc(commands.Cog):
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Error: {str(e)}")
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def insult(self, ctx, *, recipient: Optional[str] = None) -> None:
|
||||
"""
|
||||
Insult Someone (or yourself)
|
||||
"""
|
||||
try:
|
||||
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
|
||||
if not guild:
|
||||
return
|
||||
authorDisplay: str = (
|
||||
ctx.author.display_name
|
||||
if (ctx.author.display_name is not None)
|
||||
else ctx.message.author.display_name
|
||||
)
|
||||
|
||||
if not recipient:
|
||||
recipient = authorDisplay.strip()
|
||||
else:
|
||||
if discord.utils.raw_mentions(recipient):
|
||||
# There are mentions
|
||||
recipient_id: int = discord.utils.raw_mentions(recipient)[
|
||||
0
|
||||
] # First mention
|
||||
recipient_member: Optional[discord.Member] = guild.get_member(
|
||||
recipient_id
|
||||
)
|
||||
if not recipient_member:
|
||||
return
|
||||
recipient = recipient_member.display_name
|
||||
else:
|
||||
recipient = discord.utils.escape_mentions(recipient.strip())
|
||||
with ctx.channel.typing():
|
||||
insult: str = await self.util.get_insult(recipient)
|
||||
if insult:
|
||||
return await ctx.respond(insult)
|
||||
return await ctx.respond("Insult failed :(")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Insult failed :(\nError: {str(e)}")
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def compliment(
|
||||
self, ctx, *, recipient: Optional[str] = None, language: Optional[str] = "en"
|
||||
@@ -644,53 +587,6 @@ class Misc(commands.Cog):
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Failed: {str(e)}")
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def cyanide(self, ctx, *, recipient: Optional[str] = None) -> None:
|
||||
"""
|
||||
Cyanide!
|
||||
"""
|
||||
authorDisplay: str = (
|
||||
ctx.author.display_name
|
||||
if (ctx.author.display_name is not None)
|
||||
else ctx.message.author.display_name
|
||||
)
|
||||
|
||||
if not recipient:
|
||||
recipient = authorDisplay.strip()
|
||||
recipient_normal: str = ctx.author.mention
|
||||
else:
|
||||
recipient_normal = recipient
|
||||
if discord.utils.raw_mentions(recipient):
|
||||
# There are mentions
|
||||
recipient_id: int = discord.utils.raw_mentions(recipient)[
|
||||
0
|
||||
] # First mention
|
||||
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
|
||||
if not guild:
|
||||
return
|
||||
recipient_member: Optional[discord.Member] = guild.get_member(
|
||||
recipient_id
|
||||
)
|
||||
if not recipient_member:
|
||||
return
|
||||
recipient = recipient_member.display_name
|
||||
recipient_normal = recipient_member.mention
|
||||
else:
|
||||
recipient = discord.utils.escape_mentions(recipient.strip())
|
||||
try:
|
||||
response = await ctx.respond(
|
||||
f"*doses **{recipient_normal}** with Zyklon-B*"
|
||||
)
|
||||
await self.util.increment_counter("cyanides")
|
||||
try:
|
||||
await response.add_reaction(emoji="☠️")
|
||||
return await response.add_reaction(emoji="🇩🇪")
|
||||
except Exception as e:
|
||||
logging.debug("Failed to add cynaide reaction: %s", str(e))
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Failed: {str(e)}")
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def gravy(self, ctx, *, recipient: Optional[str] = None) -> None:
|
||||
"""
|
||||
@@ -784,48 +680,6 @@ class Misc(commands.Cog):
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Failed: {str(e)}")
|
||||
|
||||
@bridge.bridge_command(aliases=["bully"])
|
||||
async def shove(self, ctx, *, recipient: Optional[str] = None) -> None:
|
||||
"""
|
||||
Shove someone! (Or yourself)
|
||||
"""
|
||||
chosen_fate: str = random.choice(self.FATES)
|
||||
|
||||
authorDisplay: str = (
|
||||
ctx.author.display_name
|
||||
if (ctx.author.display_name is not None)
|
||||
else ctx.message.author.display_name
|
||||
)
|
||||
|
||||
if not recipient:
|
||||
recipient = authorDisplay.strip()
|
||||
recipient_normal: str = ctx.author.mention
|
||||
else:
|
||||
recipient_normal = recipient
|
||||
if discord.utils.raw_mentions(recipient):
|
||||
# There are mentions
|
||||
recipient_id: int = discord.utils.raw_mentions(recipient)[
|
||||
0
|
||||
] # First mention
|
||||
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
|
||||
if not guild:
|
||||
return
|
||||
recipient_member: Optional[discord.Member] = guild.get_member(
|
||||
recipient_id
|
||||
)
|
||||
if not recipient_member:
|
||||
return
|
||||
recipient = recipient_member.display_name
|
||||
recipient_normal = recipient_member.mention
|
||||
else:
|
||||
recipient = discord.utils.escape_mentions(recipient.strip())
|
||||
try:
|
||||
await ctx.respond(f"*shoves **{recipient_normal}** {chosen_fate}*")
|
||||
await self.util.increment_counter("shoves")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Failed: {str(e)}")
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def coffee(self, ctx, *, recipient: Optional[str] = None) -> None:
|
||||
"""
|
||||
@@ -968,50 +822,6 @@ class Misc(commands.Cog):
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Failed: {str(e)}")
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def ritalini(self, ctx, *, recipient: Optional[str] = None) -> None:
|
||||
"""
|
||||
Ritalini!
|
||||
"""
|
||||
authorDisplay = (
|
||||
ctx.author.display_name
|
||||
if (ctx.author.display_name is not None)
|
||||
else ctx.message.author.display_name
|
||||
)
|
||||
|
||||
if not recipient:
|
||||
recipient = authorDisplay.strip()
|
||||
recipient_normal: str = ctx.author.mention
|
||||
else:
|
||||
recipient_normal = recipient
|
||||
if discord.utils.raw_mentions(recipient):
|
||||
# There are mentions
|
||||
recipient_id: int = discord.utils.raw_mentions(recipient)[
|
||||
0
|
||||
] # First mention
|
||||
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
|
||||
if not guild:
|
||||
return
|
||||
recipient_member: Optional[discord.Member] = guild.get_member(
|
||||
recipient_id
|
||||
)
|
||||
if not recipient_member:
|
||||
return
|
||||
recipient = recipient_member.display_name
|
||||
recipient_normal = recipient_member.mention
|
||||
else:
|
||||
recipient = discord.utils.escape_mentions(recipient.strip())
|
||||
try:
|
||||
response = await ctx.respond(
|
||||
f"*serves **{recipient_normal}** a plate of ritalini* 😉"
|
||||
)
|
||||
await response.add_reaction(emoji="💊")
|
||||
await response.add_reaction(emoji="🍝")
|
||||
await self.util.increment_counter("ritalinis")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Failed: {str(e)}")
|
||||
|
||||
@bridge.bridge_command(aliases=["gc"])
|
||||
async def grilledcheese(self, ctx, *, recipient: Optional[str] = None) -> None:
|
||||
"""
|
||||
@@ -1186,104 +996,6 @@ class Misc(commands.Cog):
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Failed: {str(e)}")
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def hang(self, ctx, *, recipient: Optional[str] = None) -> None:
|
||||
"""
|
||||
Hang someone!
|
||||
"""
|
||||
authorDisplay = (
|
||||
ctx.author.display_name
|
||||
if (ctx.author.display_name is not None)
|
||||
else ctx.message.author.display_name
|
||||
)
|
||||
|
||||
if not recipient:
|
||||
recipient = authorDisplay.strip()
|
||||
recipient_normal: str = ctx.author.mention
|
||||
else:
|
||||
recipient_normal = recipient
|
||||
if discord.utils.raw_mentions(recipient):
|
||||
# There are mentions
|
||||
recipient_id: int = discord.utils.raw_mentions(recipient)[
|
||||
0
|
||||
] # First mention
|
||||
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
|
||||
if not guild:
|
||||
return
|
||||
recipient_member: Optional[discord.Member] = guild.get_member(
|
||||
recipient_id
|
||||
)
|
||||
if not recipient_member:
|
||||
return
|
||||
recipient = recipient_member.display_name
|
||||
recipient_normal = recipient_member.mention
|
||||
else:
|
||||
recipient = discord.utils.escape_mentions(recipient.strip())
|
||||
try:
|
||||
response = await ctx.respond(
|
||||
f"*sends **{recipient_normal}** to the Gallows to be hanged asynchronely*"
|
||||
)
|
||||
await self.util.increment_counter("hangings")
|
||||
try:
|
||||
return await response.add_reaction(emoji="☠️")
|
||||
except Exception as e:
|
||||
logging.debug("Failed to add hang reaction: %s", str(e))
|
||||
except Exception as e:
|
||||
await ctx.respond(f"Failed: {str(e)}")
|
||||
traceback.print_exc()
|
||||
return
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def touch(self, ctx, *, recipient: Optional[str] = None) -> None:
|
||||
"""
|
||||
Touch someone!
|
||||
"""
|
||||
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
|
||||
if not guild:
|
||||
return
|
||||
no_self_touch: str = ", don't fucking touch yourself here. You disgust me."
|
||||
|
||||
if not recipient:
|
||||
recipient_normal: str = ctx.author.mention
|
||||
await ctx.respond(f"{recipient_normal}{no_self_touch}")
|
||||
try:
|
||||
await ctx.message.add_reaction(emoji="🤮")
|
||||
except Exception as e:
|
||||
logging.debug(
|
||||
"Failed to add puke reactin for touch command: %s", str(e)
|
||||
)
|
||||
await self.util.increment_counter("touch_denials")
|
||||
return
|
||||
else:
|
||||
recipient_normal = recipient
|
||||
if discord.utils.raw_mentions(recipient):
|
||||
# There are mentions
|
||||
recipient_id: int = discord.utils.raw_mentions(recipient)[
|
||||
0
|
||||
] # First mention
|
||||
if not guild:
|
||||
return
|
||||
recipient_member: Optional[discord.Member] = guild.get_member(
|
||||
recipient_id
|
||||
)
|
||||
if not recipient_member:
|
||||
return
|
||||
recipient = recipient_member.display_name
|
||||
recipient_normal = recipient_member.mention
|
||||
else:
|
||||
recipient = discord.utils.escape_mentions(recipient.strip())
|
||||
try:
|
||||
response = await ctx.respond(
|
||||
f"*touches **{recipient_normal}** for **{ctx.author.mention}** because they wouldn't touch them with a shitty stick!*"
|
||||
)
|
||||
await self.util.increment_counter("touches")
|
||||
try:
|
||||
return await response.add_reaction(emoji="👉")
|
||||
except Exception as e:
|
||||
logging.debug("Failed to add touch reaction: %s", str(e))
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Failed: {str(e)}")
|
||||
|
||||
@bridge.bridge_command() # type: ignore
|
||||
@is_spamchan_or_drugs()
|
||||
|
||||
429
cogs/owner.py
429
cogs/owner.py
@@ -3,13 +3,16 @@ import random
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Optional
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
import discord
|
||||
import requests
|
||||
from discord.ext import bridge, commands
|
||||
from disc_havoc import Havoc
|
||||
import util
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cogs.message_logger import MessageLogger
|
||||
|
||||
|
||||
class Owner(commands.Cog):
|
||||
"""Owner Cog for Havoc"""
|
||||
@@ -167,9 +170,9 @@ class Owner(commands.Cog):
|
||||
if message.attachments:
|
||||
for item in message.attachments:
|
||||
if item.url and len(item.url) >= 20:
|
||||
image: io.BytesIO = io.BytesIO(
|
||||
requests.get(item.url, stream=True, timeout=20).raw.read()
|
||||
)
|
||||
response = requests.get(item.url, stream=True, timeout=20)
|
||||
image_data: bytes = response.raw.read() or b''
|
||||
image: io.BytesIO = io.BytesIO(image_data)
|
||||
ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
|
||||
_file = discord.File(image, filename=f"img.{ext}")
|
||||
if not _file:
|
||||
@@ -210,9 +213,9 @@ class Owner(commands.Cog):
|
||||
if message.attachments:
|
||||
for item in message.attachments:
|
||||
if item.url and len(item.url) >= 20:
|
||||
image: io.BytesIO = io.BytesIO(
|
||||
requests.get(item.url, stream=True, timeout=20).raw.read()
|
||||
)
|
||||
response = requests.get(item.url, stream=True, timeout=20)
|
||||
image_data: bytes = response.raw.read() or b''
|
||||
image: io.BytesIO = io.BytesIO(image_data)
|
||||
ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
|
||||
_file = discord.File(image, filename=f"img.{ext}")
|
||||
if not _file:
|
||||
@@ -254,9 +257,9 @@ class Owner(commands.Cog):
|
||||
if message.attachments:
|
||||
for item in message.attachments:
|
||||
if item.url and len(item.url) >= 20:
|
||||
image: io.BytesIO = io.BytesIO(
|
||||
requests.get(item.url, stream=True, timeout=20).raw.read()
|
||||
)
|
||||
response = requests.get(item.url, stream=True, timeout=20)
|
||||
image_data: bytes = response.raw.read() or b''
|
||||
image: io.BytesIO = io.BytesIO(image_data)
|
||||
ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
|
||||
_file = discord.File(image, filename=f"img.{ext}")
|
||||
await funhouse_channel.send(
|
||||
@@ -340,6 +343,412 @@ class Owner(commands.Cog):
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"ERR: {str(e)}", ephemeral=True)
|
||||
|
||||
def _get_message_logger(self) -> Optional["MessageLogger"]:
|
||||
"""Get the MessageLogger cog with proper typing."""
|
||||
cog = self.bot.get_cog('MessageLogger')
|
||||
if cog is None:
|
||||
return None
|
||||
return cog # type: ignore[return-value]
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def db_stats(self, ctx) -> None:
|
||||
"""Get database statistics including image cache info."""
|
||||
try:
|
||||
# Get the message logger cog to access the database
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
stats = await logger_cog.db.get_stats()
|
||||
uncached = await logger_cog.db.get_uncached_counts()
|
||||
|
||||
embed = discord.Embed(title="Database Statistics", color=discord.Color.blue())
|
||||
embed.add_field(name="Messages", value=f"{stats.get('total_messages', 0):,}", inline=True)
|
||||
embed.add_field(name="Users", value=f"{stats.get('total_users', 0):,}", inline=True)
|
||||
embed.add_field(name="Guilds", value=f"{stats.get('total_guilds', 0):,}", inline=True)
|
||||
embed.add_field(name="Channels", value=f"{stats.get('total_channels', 0):,}", inline=True)
|
||||
embed.add_field(name="Attachments", value=f"{stats.get('total_attachments', 0):,}", inline=True)
|
||||
embed.add_field(name="Reactions", value=f"{stats.get('total_reactions', 0):,}", inline=True)
|
||||
embed.add_field(name="Cached Images", value=f"{stats.get('cached_images', 0):,}", inline=True)
|
||||
embed.add_field(name="Cache Size", value=f"{stats.get('cached_images_size_mb', 0):.2f} MB", inline=True)
|
||||
embed.add_field(name="Deleted Messages", value=f"{stats.get('deleted_messages', 0):,}", inline=True)
|
||||
|
||||
# Uncached counts
|
||||
uncached_text = (
|
||||
f"Avatars: {uncached.get('users_missing_avatars', 0):,}\n"
|
||||
f"Banners: {uncached.get('users_missing_banners', 0):,}\n"
|
||||
f"Guild Avatars: {uncached.get('members_missing_guild_avatars', 0):,}\n"
|
||||
f"Attachments: {uncached.get('attachments_missing_cache', 0):,}"
|
||||
)
|
||||
embed.add_field(name="Uncached Images", value=uncached_text, inline=False)
|
||||
|
||||
# Cache breakdown by type
|
||||
if stats.get('cached_by_type'):
|
||||
breakdown = "\n".join(f"{k}: {v:,}" for k, v in stats['cached_by_type'].items())
|
||||
embed.add_field(name="Cache by Type", value=breakdown or "None", inline=False)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting db stats: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def backfill_avatars(self, ctx, batch_size: int = 50, max_batches: int = 10) -> None:
|
||||
"""Backfill missing avatar images into the cache."""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
await ctx.respond(f"Starting avatar backfill (batch_size={batch_size}, max_batches={max_batches})...")
|
||||
|
||||
stats = await logger_cog.db.backfill_missing_avatars(batch_size, max_batches)
|
||||
|
||||
embed = discord.Embed(title="Avatar Backfill Complete", color=discord.Color.green())
|
||||
embed.add_field(name="Avatars", value=f"{stats['avatars_cached']}/{stats['avatars_processed']} cached", inline=True)
|
||||
embed.add_field(name="Banners", value=f"{stats['banners_cached']}/{stats['banners_processed']} cached", inline=True)
|
||||
embed.add_field(name="Guild Avatars", value=f"{stats['guild_avatars_cached']}/{stats['guild_avatars_processed']} cached", inline=True)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in backfill_avatars: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def backfill_attachments(self, ctx, batch_size: int = 50, max_batches: int = 10) -> None:
|
||||
"""Backfill missing attachment images into the cache."""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
await ctx.respond(f"Starting attachment backfill (batch_size={batch_size}, max_batches={max_batches})...")
|
||||
|
||||
stats = await logger_cog.db.backfill_missing_attachments(batch_size, max_batches)
|
||||
|
||||
embed = discord.Embed(title="Attachment Backfill Complete", color=discord.Color.green())
|
||||
embed.add_field(name="Attachments", value=f"{stats['attachments_cached']}/{stats['attachments_processed']} cached", inline=True)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in backfill_attachments: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def reset_history(self, ctx, guild_id: Optional[str] = None) -> None:
|
||||
"""Reset history fetch progress to re-fetch messages (and reactions)."""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db or not logger_cog.db.pool:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
pool = logger_cog.db.pool
|
||||
assert pool is not None # Already checked above
|
||||
async with pool.acquire() as conn:
|
||||
if guild_id:
|
||||
# Reset for specific guild
|
||||
gid = int(guild_id)
|
||||
await conn.execute("""
|
||||
UPDATE history_fetch_progress SET is_complete = FALSE, messages_fetched = 0
|
||||
WHERE channel_id IN (SELECT channel_id FROM channels WHERE guild_id = $1)
|
||||
""", gid)
|
||||
await ctx.respond(f"Reset history progress for guild {guild_id}. Re-queueing channels...")
|
||||
|
||||
# Re-queue channels for this guild
|
||||
guild = self.bot.get_guild(gid)
|
||||
if guild:
|
||||
for channel in guild.channels:
|
||||
if isinstance(channel, (discord.TextChannel, discord.Thread)):
|
||||
await logger_cog.history_fetch_queue.put(channel)
|
||||
await ctx.respond(f"Queued {len([c for c in guild.channels if isinstance(c, (discord.TextChannel, discord.Thread))])} channels for re-fetch.")
|
||||
else:
|
||||
# Reset all
|
||||
await conn.execute("UPDATE history_fetch_progress SET is_complete = FALSE, messages_fetched = 0")
|
||||
await ctx.respond("Reset all history progress. Re-queueing all channels...")
|
||||
|
||||
# Re-queue all channels
|
||||
await logger_cog._queue_all_channels_for_history()
|
||||
await ctx.respond(f"Queued {logger_cog.history_fetch_queue.qsize()} channels for re-fetch.")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in reset_history: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def history_status(self, ctx) -> None:
|
||||
"""Check the status of history fetching."""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db or not logger_cog.db.pool:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
queue_size = logger_cog.history_fetch_queue.qsize()
|
||||
is_fetching = logger_cog.is_fetching_history
|
||||
|
||||
pool = logger_cog.db.pool
|
||||
assert pool is not None # Already checked above
|
||||
async with pool.acquire() as conn:
|
||||
total_channels = await conn.fetchval("SELECT COUNT(*) FROM history_fetch_progress")
|
||||
complete = await conn.fetchval("SELECT COUNT(*) FROM history_fetch_progress WHERE is_complete = TRUE")
|
||||
incomplete = await conn.fetchval("SELECT COUNT(*) FROM history_fetch_progress WHERE is_complete = FALSE")
|
||||
total_fetched = await conn.fetchval("SELECT SUM(messages_fetched) FROM history_fetch_progress")
|
||||
|
||||
embed = discord.Embed(title="History Fetch Status", color=discord.Color.blue())
|
||||
embed.add_field(name="Queue Size", value=f"{queue_size} channels", inline=True)
|
||||
embed.add_field(name="Is Fetching", value="Yes" if is_fetching else "No", inline=True)
|
||||
embed.add_field(name="Channels Tracked", value=f"{total_channels or 0}", inline=True)
|
||||
embed.add_field(name="Complete", value=f"{complete or 0}", inline=True)
|
||||
embed.add_field(name="Incomplete", value=f"{incomplete or 0}", inline=True)
|
||||
embed.add_field(name="Messages Fetched", value=f"{total_fetched or 0:,}", inline=True)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in history_status: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def video_status(self, ctx) -> None:
|
||||
"""Check video caching status and failures."""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db or not logger_cog.db.pool:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
counts = await logger_cog.db.get_uncached_video_counts()
|
||||
|
||||
embed = discord.Embed(title="Video Cache Status", color=discord.Color.blue())
|
||||
embed.add_field(name="Cached Videos", value=f"{counts.get('total_cached_videos', 0):,}", inline=True)
|
||||
embed.add_field(name="Total Size", value=f"{counts.get('total_cached_videos_size_gb', 0):.2f} GB", inline=True)
|
||||
embed.add_field(name="Failed Downloads", value=f"{counts.get('failed_video_downloads', 0):,}", inline=True)
|
||||
embed.add_field(name="Attachments Pending", value=f"{counts.get('attachments_missing_video_cache', 0):,}", inline=True)
|
||||
embed.add_field(name="Embeds Pending", value=f"{counts.get('embeds_missing_video_cache', 0):,}", inline=True)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in video_status: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def video_failures(self, ctx, limit: int = 10) -> None:
|
||||
"""Show recent video download failures."""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db or not logger_cog.db.pool:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
failures = await logger_cog.db.get_video_download_failures(limit)
|
||||
|
||||
if not failures:
|
||||
return await ctx.respond("No video download failures recorded.", ephemeral=True)
|
||||
|
||||
lines = []
|
||||
for f in failures:
|
||||
url = f['source_url'][:60] + "..." if len(f['source_url']) > 60 else f['source_url']
|
||||
error = f['error_message'][:80] + "..." if f['error_message'] and len(f['error_message']) > 80 else f['error_message']
|
||||
lines.append(f"**{f['source_type']}** (x{f['error_count']}): `{url}`\n └ {error}")
|
||||
|
||||
content = "\n".join(lines[:10]) # Limit to avoid message too long
|
||||
embed = discord.Embed(title=f"Video Failures (showing {len(failures)})", description=content, color=discord.Color.red())
|
||||
await ctx.respond(embed=embed)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in video_failures: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def video_retry(self, ctx, older_than_days: int = 7) -> None:
|
||||
"""Clear old video failures to allow retrying downloads."""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db or not logger_cog.db.pool:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
deleted = await logger_cog.db.clear_video_failures(older_than_days)
|
||||
await ctx.respond(f"Cleared {deleted} video failures older than {older_than_days} days. They will be retried on next backfill cycle.", ephemeral=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in video_retry: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def video_retry_all(self, ctx) -> None:
|
||||
"""Clear ALL video failures to retry everything."""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db or not logger_cog.db.pool:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
pool = logger_cog.db.pool
|
||||
async with pool.acquire() as conn:
|
||||
result = await conn.execute("DELETE FROM video_download_failures")
|
||||
try:
|
||||
deleted = int(result.split()[-1])
|
||||
except Exception:
|
||||
deleted = 0
|
||||
|
||||
await ctx.respond(f"Cleared {deleted} video failures. All will be retried on next backfill cycle.", ephemeral=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in video_retry_all: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def video_retry_fetch(self, ctx, limit: int = 20) -> None:
|
||||
"""Attempt to refresh attachment URLs for recent failed attachment downloads.
|
||||
|
||||
This will look up failures for source_type='attachment', find the related message
|
||||
and attachment by filename, update the URL in the DB, and remove the failure
|
||||
so the next backfill cycle retries it.
|
||||
"""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db or not logger_cog.db.pool:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
pool = logger_cog.db.pool
|
||||
async with pool.acquire() as conn:
|
||||
failures = await conn.fetch("SELECT source_url, source_type FROM video_download_failures WHERE source_type = 'attachment' ORDER BY last_failure DESC LIMIT $1", limit)
|
||||
|
||||
if not failures:
|
||||
return await ctx.respond("No recent attachment download failures found.", ephemeral=True)
|
||||
|
||||
found = 0
|
||||
updated = 0
|
||||
not_found = []
|
||||
|
||||
for f in failures:
|
||||
found += 1
|
||||
src_url = f['source_url']
|
||||
# Find attachment row matching this url
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT attachment_id, message_id, filename FROM attachments WHERE url = $1 LIMIT 1", src_url)
|
||||
|
||||
if not row:
|
||||
not_found.append(src_url)
|
||||
continue
|
||||
|
||||
attachment_id = row['attachment_id']
|
||||
message_id = row['message_id']
|
||||
filename = row['filename']
|
||||
|
||||
# Lookup message channel
|
||||
async with pool.acquire() as conn:
|
||||
ch_row = await conn.fetchrow("SELECT channel_id FROM messages WHERE message_id = $1 LIMIT 1", message_id)
|
||||
|
||||
if not ch_row or not ch_row.get('channel_id'):
|
||||
not_found.append(src_url)
|
||||
continue
|
||||
|
||||
channel_id = ch_row['channel_id']
|
||||
channel = self.bot.get_channel(channel_id)
|
||||
if not channel:
|
||||
not_found.append(src_url)
|
||||
continue
|
||||
|
||||
try:
|
||||
# channel might be a cached object without fetch_message; try to fetch if necessary
|
||||
if not hasattr(channel, 'fetch_message'):
|
||||
try:
|
||||
channel = await self.bot.fetch_channel(channel_id)
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
# Use getattr and hasattr so static analyzers don't complain
|
||||
if hasattr(channel, 'fetch_message'):
|
||||
fetch = getattr(channel, 'fetch_message')
|
||||
message = await fetch(message_id)
|
||||
else:
|
||||
raise RuntimeError("channel doesn't support fetch_message")
|
||||
except Exception:
|
||||
not_found.append(src_url)
|
||||
continue
|
||||
|
||||
# Find updated attachment by filename
|
||||
new_url = None
|
||||
for att in message.attachments:
|
||||
if att.filename == filename:
|
||||
new_url = att.url
|
||||
break
|
||||
|
||||
if not new_url:
|
||||
not_found.append(src_url)
|
||||
continue
|
||||
|
||||
# Update DB with new URL and remove failure
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute("UPDATE attachments SET url = $1 WHERE attachment_id = $2", new_url, attachment_id)
|
||||
await conn.execute("DELETE FROM video_download_failures WHERE source_url = $1", src_url)
|
||||
updated += 1
|
||||
|
||||
resp = f"Scanned {found} failures; updated {updated} attachment URLs; {len(not_found)} not updated."
|
||||
if not_found:
|
||||
resp += "\nNot updated examples: " + ", ".join(not_found[:3])
|
||||
|
||||
await ctx.respond(resp, ephemeral=True)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in video_retry_fetch: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def video_backfill(self, ctx, batch_size: int = 10, max_batches: int = 0) -> None:
|
||||
"""Trigger a video backfill run. Set max_batches=0 (default) for unlimited.
|
||||
|
||||
Usage:
|
||||
.video_backfill (unlimited with batch_size=10)
|
||||
.video_backfill 25 (unlimited with batch_size=25)
|
||||
.video_backfill 10 100 (batch_size=10, max_batches=100)
|
||||
"""
|
||||
try:
|
||||
logger_cog = self._get_message_logger()
|
||||
if not logger_cog or not logger_cog.db or not logger_cog.db.pool:
|
||||
return await ctx.respond("Database not available.", ephemeral=True)
|
||||
|
||||
# Check counts first
|
||||
counts = await logger_cog.db.get_uncached_video_counts()
|
||||
msg_pending = counts.get('messages_with_uncached_videos', 0)
|
||||
att_pending = counts.get('attachments_missing_video_cache', 0)
|
||||
emb_pending = counts.get('embeds_missing_video_cache', 0)
|
||||
|
||||
if msg_pending == 0:
|
||||
return await ctx.respond(f"No videos need caching. (Cached: {counts.get('total_cached_videos', 0)}, Failed: {counts.get('failed_video_downloads', 0)})", ephemeral=True)
|
||||
|
||||
# If max_batches=0, pass None to mean unlimited batches
|
||||
cap = None if max_batches == 0 else max_batches
|
||||
|
||||
await ctx.respond(f"Starting video backfill (batch_size={batch_size}, max_batches={'unlimited' if cap is None else max_batches})...\nMessages with uncached videos: {msg_pending} ({att_pending} attachments, {emb_pending} embeds)", ephemeral=True)
|
||||
|
||||
stats = await logger_cog.db.backfill_missing_videos(batch_size=batch_size, max_batches=cap)
|
||||
|
||||
embed = discord.Embed(title="Video Backfill Complete", color=discord.Color.green())
|
||||
embed.add_field(name="Attachments Processed", value=str(stats.get('attachments_processed', 0)), inline=True)
|
||||
embed.add_field(name="Attachments Cached", value=str(stats.get('attachments_cached', 0)), inline=True)
|
||||
embed.add_field(name="Embeds Processed", value=str(stats.get('embeds_processed', 0)), inline=True)
|
||||
embed.add_field(name="Embeds Cached", value=str(stats.get('embeds_cached', 0)), inline=True)
|
||||
await ctx.respond(embed=embed, ephemeral=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in video_backfill: {e}")
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"Error: {str(e)}", ephemeral=True)
|
||||
|
||||
|
||||
def setup(bot) -> None:
|
||||
"""Run on Cog Load"""
|
||||
|
||||
Reference in New Issue
Block a user