bh
This commit is contained in:
parent
29732f28d9
commit
59ef883aef
373
cogs/starboard_cog.py
Normal file
373
cogs/starboard_cog.py
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from discord import app_commands
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
from typing import Optional, Union, Dict, List, Tuple
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Add the parent directory to sys.path to allow imports
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
# Import settings manager for database operations
|
||||||
|
import discordbot.settings_manager as settings_manager
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class StarboardCog(commands.Cog):
|
||||||
|
"""A cog that implements a starboard feature for highlighting popular messages."""
|
||||||
|
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.emoji_pattern = re.compile(r'<a?:.+?:\d+>|[\U00010000-\U0010ffff]')
|
||||||
|
self.pending_updates = {} # Store message IDs that are being processed to prevent race conditions
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_raw_reaction_add(self, payload):
|
||||||
|
"""Event listener for when a reaction is added to a message."""
|
||||||
|
# Skip if the reaction is from a bot
|
||||||
|
if payload.member.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get guild and check if it exists
|
||||||
|
guild = self.bot.get_guild(payload.guild_id)
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get starboard settings for this guild
|
||||||
|
settings = await settings_manager.get_starboard_settings(guild.id)
|
||||||
|
if not settings or not settings.get('enabled') or not settings.get('starboard_channel_id'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the emoji matches the configured star emoji
|
||||||
|
emoji_str = str(payload.emoji)
|
||||||
|
if emoji_str != settings.get('star_emoji', '⭐'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process the star reaction
|
||||||
|
await self._process_star_reaction(payload, settings, is_add=True)
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_raw_reaction_remove(self, payload):
|
||||||
|
"""Event listener for when a reaction is removed from a message."""
|
||||||
|
# Get guild and check if it exists
|
||||||
|
guild = self.bot.get_guild(payload.guild_id)
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get user who removed the reaction
|
||||||
|
user = await self.bot.fetch_user(payload.user_id)
|
||||||
|
if not user or user.bot:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get starboard settings for this guild
|
||||||
|
settings = await settings_manager.get_starboard_settings(guild.id)
|
||||||
|
if not settings or not settings.get('enabled') or not settings.get('starboard_channel_id'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the emoji matches the configured star emoji
|
||||||
|
emoji_str = str(payload.emoji)
|
||||||
|
if emoji_str != settings.get('star_emoji', '⭐'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process the star reaction removal
|
||||||
|
await self._process_star_reaction(payload, settings, is_add=False)
|
||||||
|
|
||||||
|
async def _process_star_reaction(self, payload, settings, is_add: bool):
|
||||||
|
"""Process a star reaction being added or removed."""
|
||||||
|
# Get the channels
|
||||||
|
guild = self.bot.get_guild(payload.guild_id)
|
||||||
|
source_channel = guild.get_channel(payload.channel_id)
|
||||||
|
starboard_channel = guild.get_channel(settings.get('starboard_channel_id'))
|
||||||
|
|
||||||
|
if not source_channel or not starboard_channel:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the source channel is the starboard channel (prevent stars on starboard posts)
|
||||||
|
if source_channel.id == starboard_channel.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Acquire lock for this message to prevent race conditions
|
||||||
|
message_key = f"{payload.guild_id}:{payload.message_id}"
|
||||||
|
if message_key in self.pending_updates:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.pending_updates[message_key] = True
|
||||||
|
try:
|
||||||
|
# Get the message
|
||||||
|
try:
|
||||||
|
message = await source_channel.fetch_message(payload.message_id)
|
||||||
|
except discord.NotFound:
|
||||||
|
return
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
log.error(f"Error fetching message {payload.message_id}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if message is from a bot and if we should ignore bot messages
|
||||||
|
if message.author.bot and settings.get('ignore_bots', True):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the user is starring their own message and if that's allowed
|
||||||
|
if is_add and payload.user_id == message.author.id and not settings.get('self_star', False):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update the reaction in the database
|
||||||
|
if is_add:
|
||||||
|
star_count = await settings_manager.add_starboard_reaction(
|
||||||
|
guild.id, message.id, payload.user_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
star_count = await settings_manager.remove_starboard_reaction(
|
||||||
|
guild.id, message.id, payload.user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# If we couldn't get a valid count, fetch it directly
|
||||||
|
if not isinstance(star_count, int):
|
||||||
|
star_count = await settings_manager.get_starboard_reaction_count(guild.id, message.id)
|
||||||
|
|
||||||
|
# Get the threshold from settings
|
||||||
|
threshold = settings.get('threshold', 3)
|
||||||
|
|
||||||
|
# Check if this message is already in the starboard
|
||||||
|
entry = await settings_manager.get_starboard_entry(guild.id, message.id)
|
||||||
|
|
||||||
|
if star_count >= threshold:
|
||||||
|
# Message should be in starboard
|
||||||
|
if entry:
|
||||||
|
# Update existing entry
|
||||||
|
try:
|
||||||
|
starboard_message = await starboard_channel.fetch_message(entry.get('starboard_message_id'))
|
||||||
|
await self._update_starboard_message(starboard_message, message, star_count)
|
||||||
|
await settings_manager.update_starboard_entry(guild.id, message.id, star_count)
|
||||||
|
except discord.NotFound:
|
||||||
|
# Starboard message was deleted, create a new one
|
||||||
|
starboard_message = await self._create_starboard_message(starboard_channel, message, star_count)
|
||||||
|
if starboard_message:
|
||||||
|
await settings_manager.create_starboard_entry(
|
||||||
|
guild.id, message.id, source_channel.id,
|
||||||
|
starboard_message.id, message.author.id, star_count
|
||||||
|
)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
log.error(f"Error updating starboard message: {e}")
|
||||||
|
else:
|
||||||
|
# Create new entry
|
||||||
|
starboard_message = await self._create_starboard_message(starboard_channel, message, star_count)
|
||||||
|
if starboard_message:
|
||||||
|
await settings_manager.create_starboard_entry(
|
||||||
|
guild.id, message.id, source_channel.id,
|
||||||
|
starboard_message.id, message.author.id, star_count
|
||||||
|
)
|
||||||
|
elif entry:
|
||||||
|
# Message is below threshold but exists in starboard
|
||||||
|
try:
|
||||||
|
# Delete the starboard message if it exists
|
||||||
|
starboard_message = await starboard_channel.fetch_message(entry.get('starboard_message_id'))
|
||||||
|
await starboard_message.delete()
|
||||||
|
except (discord.NotFound, discord.HTTPException):
|
||||||
|
pass # Message already deleted or couldn't be deleted
|
||||||
|
|
||||||
|
# Delete the entry from the database
|
||||||
|
# Note: We don't have a dedicated function for this yet, but we could add one
|
||||||
|
# For now, we'll just update the star count
|
||||||
|
await settings_manager.update_starboard_entry(guild.id, message.id, star_count)
|
||||||
|
finally:
|
||||||
|
# Release the lock
|
||||||
|
self.pending_updates.pop(message_key, None)
|
||||||
|
|
||||||
|
async def _create_starboard_message(self, starboard_channel, message, star_count):
|
||||||
|
"""Create a new message in the starboard channel."""
|
||||||
|
try:
|
||||||
|
embed = self._create_starboard_embed(message, star_count)
|
||||||
|
|
||||||
|
# Add jump link to the original message
|
||||||
|
content = f"{self._get_star_emoji(star_count)} **{star_count}** | {message.channel.mention} | [Jump to Message]({message.jump_url})"
|
||||||
|
|
||||||
|
# Send the message to the starboard channel
|
||||||
|
return await starboard_channel.send(content=content, embed=embed)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
log.error(f"Error creating starboard message: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _update_starboard_message(self, starboard_message, original_message, star_count):
|
||||||
|
"""Update an existing message in the starboard channel."""
|
||||||
|
try:
|
||||||
|
embed = self._create_starboard_embed(original_message, star_count)
|
||||||
|
|
||||||
|
# Update the star count in the message content
|
||||||
|
content = f"{self._get_star_emoji(star_count)} **{star_count}** | {original_message.channel.mention} | [Jump to Message]({original_message.jump_url})"
|
||||||
|
|
||||||
|
# Edit the message
|
||||||
|
await starboard_message.edit(content=content, embed=embed)
|
||||||
|
return starboard_message
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
log.error(f"Error updating starboard message: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _create_starboard_embed(self, message, star_count):
|
||||||
|
"""Create an embed for the starboard message."""
|
||||||
|
embed = discord.Embed(
|
||||||
|
description=message.content,
|
||||||
|
color=0xFFAC33, # Gold color for stars
|
||||||
|
timestamp=message.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set author information
|
||||||
|
embed.set_author(
|
||||||
|
name=message.author.display_name,
|
||||||
|
icon_url=message.author.display_avatar.url
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add footer with message ID for reference
|
||||||
|
embed.set_footer(text=f"ID: {message.id}")
|
||||||
|
|
||||||
|
# Add attachments if any
|
||||||
|
if message.attachments:
|
||||||
|
# If it's an image, add it to the embed
|
||||||
|
for attachment in message.attachments:
|
||||||
|
if attachment.content_type and attachment.content_type.startswith('image/'):
|
||||||
|
embed.set_image(url=attachment.url)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add a field listing all attachments
|
||||||
|
if len(message.attachments) > 1:
|
||||||
|
attachment_list = "\n".join([f"[{a.filename}]({a.url})" for a in message.attachments])
|
||||||
|
embed.add_field(name="Attachments", value=attachment_list, inline=False)
|
||||||
|
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _get_star_emoji(self, count):
|
||||||
|
"""Get the appropriate star emoji based on the count."""
|
||||||
|
if count >= 15:
|
||||||
|
return "🌟" # Glowing star for 15+
|
||||||
|
elif count >= 10:
|
||||||
|
return "✨" # Sparkles for 10+
|
||||||
|
elif count >= 5:
|
||||||
|
return "⭐" # Star for 5+
|
||||||
|
else:
|
||||||
|
return "⭐" # Regular star for < 5
|
||||||
|
|
||||||
|
# --- Starboard Commands ---
|
||||||
|
|
||||||
|
@commands.hybrid_group(name="starboard", description="Manage the starboard settings")
|
||||||
|
@commands.has_permissions(manage_guild=True)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def starboard_group(self, ctx):
|
||||||
|
"""Commands for managing the starboard feature."""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
await ctx.send("Please specify a subcommand. Use `help starboard` for more information.")
|
||||||
|
|
||||||
|
@starboard_group.command(name="enable", description="Enable or disable the starboard")
|
||||||
|
@app_commands.describe(enabled="Whether to enable or disable the starboard")
|
||||||
|
async def starboard_enable(self, ctx, enabled: bool):
|
||||||
|
"""Enable or disable the starboard feature."""
|
||||||
|
success = await settings_manager.update_starboard_settings(ctx.guild.id, enabled=enabled)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
status = "enabled" if enabled else "disabled"
|
||||||
|
await ctx.send(f"✅ Starboard has been {status}.")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to update starboard settings.")
|
||||||
|
|
||||||
|
@starboard_group.command(name="channel", description="Set the channel for starboard posts")
|
||||||
|
@app_commands.describe(channel="The channel to use for starboard posts")
|
||||||
|
async def starboard_channel(self, ctx, channel: discord.TextChannel):
|
||||||
|
"""Set the channel where starboard messages will be posted."""
|
||||||
|
success = await settings_manager.update_starboard_settings(ctx.guild.id, starboard_channel_id=channel.id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
await ctx.send(f"✅ Starboard channel set to {channel.mention}.")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to update starboard channel.")
|
||||||
|
|
||||||
|
@starboard_group.command(name="threshold", description="Set the minimum number of stars needed")
|
||||||
|
@app_commands.describe(threshold="The minimum number of stars needed (1-25)")
|
||||||
|
async def starboard_threshold(self, ctx, threshold: int):
|
||||||
|
"""Set the minimum number of stars needed for a message to appear on the starboard."""
|
||||||
|
if threshold < 1 or threshold > 25:
|
||||||
|
await ctx.send("❌ Threshold must be between 1 and 25.")
|
||||||
|
return
|
||||||
|
|
||||||
|
success = await settings_manager.update_starboard_settings(ctx.guild.id, threshold=threshold)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
await ctx.send(f"✅ Starboard threshold set to {threshold} stars.")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to update starboard threshold.")
|
||||||
|
|
||||||
|
@starboard_group.command(name="emoji", description="Set the emoji used for starring messages")
|
||||||
|
@app_commands.describe(emoji="The emoji to use for starring messages")
|
||||||
|
async def starboard_emoji(self, ctx, emoji: str):
|
||||||
|
"""Set the emoji that will be used for starring messages."""
|
||||||
|
# Validate that the input is a single emoji
|
||||||
|
if not self.emoji_pattern.fullmatch(emoji):
|
||||||
|
await ctx.send("❌ Please provide a valid emoji.")
|
||||||
|
return
|
||||||
|
|
||||||
|
success = await settings_manager.update_starboard_settings(ctx.guild.id, star_emoji=emoji)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
await ctx.send(f"✅ Starboard emoji set to {emoji}.")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to update starboard emoji.")
|
||||||
|
|
||||||
|
@starboard_group.command(name="ignorebots", description="Set whether to ignore bot messages")
|
||||||
|
@app_commands.describe(ignore="Whether to ignore messages from bots")
|
||||||
|
async def starboard_ignorebots(self, ctx, ignore: bool):
|
||||||
|
"""Set whether messages from bots should be ignored for the starboard."""
|
||||||
|
success = await settings_manager.update_starboard_settings(ctx.guild.id, ignore_bots=ignore)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
status = "will be ignored" if ignore else "will be included"
|
||||||
|
await ctx.send(f"✅ Bot messages {status} in the starboard.")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to update bot message handling.")
|
||||||
|
|
||||||
|
@starboard_group.command(name="selfstar", description="Allow or disallow users to star their own messages")
|
||||||
|
@app_commands.describe(allow="Whether to allow users to star their own messages")
|
||||||
|
async def starboard_selfstar(self, ctx, allow: bool):
|
||||||
|
"""Set whether users can star their own messages."""
|
||||||
|
success = await settings_manager.update_starboard_settings(ctx.guild.id, self_star=allow)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
status = "can" if allow else "cannot"
|
||||||
|
await ctx.send(f"✅ Users {status} star their own messages.")
|
||||||
|
else:
|
||||||
|
await ctx.send("❌ Failed to update self-starring setting.")
|
||||||
|
|
||||||
|
@starboard_group.command(name="settings", description="Show current starboard settings")
|
||||||
|
async def starboard_settings(self, ctx):
|
||||||
|
"""Display the current starboard settings."""
|
||||||
|
settings = await settings_manager.get_starboard_settings(ctx.guild.id)
|
||||||
|
|
||||||
|
if not settings:
|
||||||
|
await ctx.send("❌ Failed to retrieve starboard settings.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create an embed to display the settings
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Starboard Settings",
|
||||||
|
color=discord.Color.gold(),
|
||||||
|
timestamp=datetime.datetime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add fields for each setting
|
||||||
|
embed.add_field(name="Status", value="Enabled" if settings.get('enabled') else "Disabled", inline=True)
|
||||||
|
|
||||||
|
channel_id = settings.get('starboard_channel_id')
|
||||||
|
channel_mention = f"<#{channel_id}>" if channel_id else "Not set"
|
||||||
|
embed.add_field(name="Channel", value=channel_mention, inline=True)
|
||||||
|
|
||||||
|
embed.add_field(name="Threshold", value=str(settings.get('threshold', 3)), inline=True)
|
||||||
|
embed.add_field(name="Emoji", value=settings.get('star_emoji', '⭐'), inline=True)
|
||||||
|
embed.add_field(name="Ignore Bots", value="Yes" if settings.get('ignore_bots', True) else "No", inline=True)
|
||||||
|
embed.add_field(name="Self-starring", value="Allowed" if settings.get('self_star', False) else "Not allowed", inline=True)
|
||||||
|
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
"""Add the cog to the bot."""
|
||||||
|
await bot.add_cog(StarboardCog(bot))
|
@ -247,6 +247,47 @@ async def initialize_database():
|
|||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Starboard Settings table - Stores configuration for the starboard feature
|
||||||
|
await conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS starboard_settings (
|
||||||
|
guild_id BIGINT PRIMARY KEY,
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
star_emoji TEXT NOT NULL DEFAULT '⭐',
|
||||||
|
threshold INTEGER NOT NULL DEFAULT 3,
|
||||||
|
starboard_channel_id BIGINT,
|
||||||
|
ignore_bots BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
self_star BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(guild_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Starboard Entries table - Tracks which messages have been reposted to the starboard
|
||||||
|
await conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS starboard_entries (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
guild_id BIGINT NOT NULL,
|
||||||
|
original_message_id BIGINT NOT NULL,
|
||||||
|
original_channel_id BIGINT NOT NULL,
|
||||||
|
starboard_message_id BIGINT NOT NULL,
|
||||||
|
author_id BIGINT NOT NULL,
|
||||||
|
star_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE(guild_id, original_message_id),
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(guild_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Starboard Reactions table - Tracks which users have starred which messages
|
||||||
|
await conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS starboard_reactions (
|
||||||
|
guild_id BIGINT NOT NULL,
|
||||||
|
message_id BIGINT NOT NULL,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (guild_id, message_id, user_id),
|
||||||
|
FOREIGN KEY (guild_id) REFERENCES guilds(guild_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
|
||||||
# Consider adding indexes later for performance on large tables
|
# Consider adding indexes later for performance on large tables
|
||||||
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_guild_settings_guild ON guild_settings (guild_id);")
|
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_guild_settings_guild ON guild_settings (guild_id);")
|
||||||
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_enabled_cogs_guild ON enabled_cogs (guild_id);")
|
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_enabled_cogs_guild ON enabled_cogs (guild_id);")
|
||||||
@ -254,10 +295,287 @@ async def initialize_database():
|
|||||||
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_command_customization_guild ON command_customization (guild_id);")
|
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_command_customization_guild ON command_customization (guild_id);")
|
||||||
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_command_group_customization_guild ON command_group_customization (guild_id);")
|
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_command_group_customization_guild ON command_group_customization (guild_id);")
|
||||||
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_command_aliases_guild ON command_aliases (guild_id);")
|
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_command_aliases_guild ON command_aliases (guild_id);")
|
||||||
|
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_starboard_entries_guild ON starboard_entries (guild_id);")
|
||||||
|
# await conn.execute("CREATE INDEX IF NOT EXISTS idx_starboard_reactions_guild ON starboard_reactions (guild_id);")
|
||||||
|
|
||||||
log.info("Database schema initialization complete.")
|
log.info("Database schema initialization complete.")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Starboard Functions ---
|
||||||
|
|
||||||
|
async def get_starboard_settings(guild_id: int):
|
||||||
|
"""Gets the starboard settings for a guild."""
|
||||||
|
if not pg_pool:
|
||||||
|
log.warning(f"PostgreSQL pool not initialized, returning None for starboard settings.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
# Check if the guild exists in the starboard_settings table
|
||||||
|
settings = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
SELECT * FROM starboard_settings WHERE guild_id = $1
|
||||||
|
""",
|
||||||
|
guild_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings:
|
||||||
|
return dict(settings)
|
||||||
|
|
||||||
|
# If no settings exist, insert default settings
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO starboard_settings (guild_id)
|
||||||
|
VALUES ($1)
|
||||||
|
ON CONFLICT (guild_id) DO NOTHING;
|
||||||
|
""",
|
||||||
|
guild_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch the newly inserted default settings
|
||||||
|
settings = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
SELECT * FROM starboard_settings WHERE guild_id = $1
|
||||||
|
""",
|
||||||
|
guild_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return dict(settings) if settings else None
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error getting starboard settings for guild {guild_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def update_starboard_settings(guild_id: int, **kwargs):
|
||||||
|
"""Updates starboard settings for a guild.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
guild_id: The ID of the guild to update settings for
|
||||||
|
**kwargs: Key-value pairs of settings to update
|
||||||
|
Possible keys: enabled, star_emoji, threshold, starboard_channel_id, ignore_bots, self_star
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not pg_pool:
|
||||||
|
log.error(f"PostgreSQL pool not initialized, cannot update starboard settings.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
valid_keys = {'enabled', 'star_emoji', 'threshold', 'starboard_channel_id', 'ignore_bots', 'self_star'}
|
||||||
|
update_dict = {k: v for k, v in kwargs.items() if k in valid_keys}
|
||||||
|
|
||||||
|
if not update_dict:
|
||||||
|
log.warning(f"No valid settings provided for starboard update for guild {guild_id}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
# Ensure guild exists
|
||||||
|
await conn.execute("INSERT INTO guilds (guild_id) VALUES ($1) ON CONFLICT (guild_id) DO NOTHING;", guild_id)
|
||||||
|
|
||||||
|
# Build the SET clause for the UPDATE statement
|
||||||
|
set_clause = ", ".join(f"{key} = ${i+2}" for i, key in enumerate(update_dict.keys()))
|
||||||
|
values = [guild_id] + list(update_dict.values())
|
||||||
|
|
||||||
|
# Update the settings
|
||||||
|
await conn.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO starboard_settings (guild_id)
|
||||||
|
VALUES ($1)
|
||||||
|
ON CONFLICT (guild_id) DO UPDATE SET {set_clause};
|
||||||
|
""",
|
||||||
|
*values
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info(f"Updated starboard settings for guild {guild_id}: {update_dict}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error updating starboard settings for guild {guild_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_starboard_entry(guild_id: int, original_message_id: int):
|
||||||
|
"""Gets a starboard entry for a specific message."""
|
||||||
|
if not pg_pool:
|
||||||
|
log.warning(f"PostgreSQL pool not initialized, returning None for starboard entry.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
entry = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
SELECT * FROM starboard_entries
|
||||||
|
WHERE guild_id = $1 AND original_message_id = $2
|
||||||
|
""",
|
||||||
|
guild_id, original_message_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return dict(entry) if entry else None
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error getting starboard entry for message {original_message_id} in guild {guild_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def create_starboard_entry(guild_id: int, original_message_id: int, original_channel_id: int,
|
||||||
|
starboard_message_id: int, author_id: int, star_count: int = 1):
|
||||||
|
"""Creates a new starboard entry."""
|
||||||
|
if not pg_pool:
|
||||||
|
log.error(f"PostgreSQL pool not initialized, cannot create starboard entry.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
# Ensure guild exists
|
||||||
|
await conn.execute("INSERT INTO guilds (guild_id) VALUES ($1) ON CONFLICT (guild_id) DO NOTHING;", guild_id)
|
||||||
|
|
||||||
|
# Create the entry
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO starboard_entries
|
||||||
|
(guild_id, original_message_id, original_channel_id, starboard_message_id, author_id, star_count)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
ON CONFLICT (guild_id, original_message_id) DO NOTHING;
|
||||||
|
""",
|
||||||
|
guild_id, original_message_id, original_channel_id, starboard_message_id, author_id, star_count
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info(f"Created starboard entry for message {original_message_id} in guild {guild_id}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error creating starboard entry for message {original_message_id} in guild {guild_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def update_starboard_entry(guild_id: int, original_message_id: int, star_count: int):
|
||||||
|
"""Updates the star count for an existing starboard entry."""
|
||||||
|
if not pg_pool:
|
||||||
|
log.error(f"PostgreSQL pool not initialized, cannot update starboard entry.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE starboard_entries
|
||||||
|
SET star_count = $3
|
||||||
|
WHERE guild_id = $1 AND original_message_id = $2
|
||||||
|
""",
|
||||||
|
guild_id, original_message_id, star_count
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info(f"Updated star count to {star_count} for message {original_message_id} in guild {guild_id}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error updating starboard entry for message {original_message_id} in guild {guild_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def add_starboard_reaction(guild_id: int, message_id: int, user_id: int):
|
||||||
|
"""Records a user's star reaction to a message."""
|
||||||
|
if not pg_pool:
|
||||||
|
log.error(f"PostgreSQL pool not initialized, cannot add starboard reaction.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
# Ensure guild exists
|
||||||
|
await conn.execute("INSERT INTO guilds (guild_id) VALUES ($1) ON CONFLICT (guild_id) DO NOTHING;", guild_id)
|
||||||
|
|
||||||
|
# Add the reaction record
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO starboard_reactions (guild_id, message_id, user_id)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (guild_id, message_id, user_id) DO NOTHING;
|
||||||
|
""",
|
||||||
|
guild_id, message_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Count total reactions for this message
|
||||||
|
count = await conn.fetchval(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) FROM starboard_reactions
|
||||||
|
WHERE guild_id = $1 AND message_id = $2
|
||||||
|
""",
|
||||||
|
guild_id, message_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return count
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error adding starboard reaction for message {message_id} in guild {guild_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def remove_starboard_reaction(guild_id: int, message_id: int, user_id: int):
|
||||||
|
"""Removes a user's star reaction from a message."""
|
||||||
|
if not pg_pool:
|
||||||
|
log.error(f"PostgreSQL pool not initialized, cannot remove starboard reaction.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
# Remove the reaction record
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM starboard_reactions
|
||||||
|
WHERE guild_id = $1 AND message_id = $2 AND user_id = $3
|
||||||
|
""",
|
||||||
|
guild_id, message_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Count remaining reactions for this message
|
||||||
|
count = await conn.fetchval(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) FROM starboard_reactions
|
||||||
|
WHERE guild_id = $1 AND message_id = $2
|
||||||
|
""",
|
||||||
|
guild_id, message_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return count
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error removing starboard reaction for message {message_id} in guild {guild_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_starboard_reaction_count(guild_id: int, message_id: int):
|
||||||
|
"""Gets the count of star reactions for a message."""
|
||||||
|
if not pg_pool:
|
||||||
|
log.warning(f"PostgreSQL pool not initialized, returning 0 for starboard reaction count.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
count = await conn.fetchval(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) FROM starboard_reactions
|
||||||
|
WHERE guild_id = $1 AND message_id = $2
|
||||||
|
""",
|
||||||
|
guild_id, message_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return count
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error getting starboard reaction count for message {message_id} in guild {guild_id}: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def has_user_reacted(guild_id: int, message_id: int, user_id: int):
|
||||||
|
"""Checks if a user has already reacted to a message."""
|
||||||
|
if not pg_pool:
|
||||||
|
log.warning(f"PostgreSQL pool not initialized, returning False for user reaction check.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with pg_pool.acquire() as conn:
|
||||||
|
result = await conn.fetchval(
|
||||||
|
"""
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM starboard_reactions
|
||||||
|
WHERE guild_id = $1 AND message_id = $2 AND user_id = $3
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
guild_id, message_id, user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Database error checking if user {user_id} reacted to message {message_id} in guild {guild_id}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# --- Helper Functions ---
|
# --- Helper Functions ---
|
||||||
def _get_redis_key(guild_id: int, key_type: str, identifier: str = None) -> str:
|
def _get_redis_key(guild_id: int, key_type: str, identifier: str = None) -> str:
|
||||||
"""Generates a standardized Redis key."""
|
"""Generates a standardized Redis key."""
|
||||||
|
54
test_starboard.py
Normal file
54
test_starboard.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import asyncio
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the parent directory to sys.path to allow imports
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
# Import the starboard cog and settings manager
|
||||||
|
from discordbot.cogs.starboard_cog import StarboardCog
|
||||||
|
import discordbot.settings_manager as settings_manager
|
||||||
|
|
||||||
|
# Load environment variables
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Set up intents
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
intents.members = True
|
||||||
|
|
||||||
|
# Create bot instance
|
||||||
|
bot = commands.Bot(command_prefix="!", intents=intents)
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
log.info(f'{bot.user.name} has connected to Discord!')
|
||||||
|
log.info(f'Bot ID: {bot.user.id}')
|
||||||
|
|
||||||
|
# Load the starboard cog
|
||||||
|
try:
|
||||||
|
await bot.add_cog(StarboardCog(bot))
|
||||||
|
log.info("StarboardCog loaded successfully!")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error loading StarboardCog: {e}")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
TOKEN = os.getenv('DISCORD_TOKEN')
|
||||||
|
if not TOKEN:
|
||||||
|
raise ValueError("No token found. Make sure to set DISCORD_TOKEN in your .env file.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.start(TOKEN)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(f"Error starting bot: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
Loading…
x
Reference in New Issue
Block a user