mirror of
https://github.com/Lilac-Rose/Lacie.git
synced 2026-04-21 14:37:32 -05:00
daily fractal: - add events/daily_fractal.py: posts today's fractal from lilacrose.dev to channel 876772600704020533 daily at noon UTC - palette name derived from seed as fallback for cached fractals - add !fractal owner-only command to manually trigger a fractal post stats: - add per_command_usage table to track usage per command name - increment_usage now accepts command name from both prefix and slash commands - expose per-command breakdown in stats payload avatar bad apple: - add invert parameter to flip dark/light regions - add real-time render and encode progress bars via interaction edits - process frames in chunks to cap peak RAM usage - track and cancel active render tasks and ffmpeg processes on cog unload - switch output to mp4 only (drop webm/alpha path), use ultrafast preset - check file size against guild limit before uploading - remove 30-second frame cap moderation: - infraction auto-removal: skip users who left and rejoined after infraction date - add Reason field to infraction removal approval and denial embeds send_embed: support zlib-compressed embed strings with legacy fallback chore: - remove events/status_alerts.py - remove arg from COG_FOLDERS - update .gitignore: add *.zip, bad_apple.mp3, */bad-apple/
183 lines
6.9 KiB
Python
183 lines
6.9 KiB
Python
import discord
|
|
from discord.ext import commands
|
|
from discord.ui import View, Button
|
|
import base64
|
|
import zlib
|
|
import json
|
|
from .loader import ModerationBase
|
|
|
|
|
|
class SendEmbedCommand(ModerationBase):
|
|
@commands.command(name="send_embed")
|
|
@ModerationBase.is_admin()
|
|
async def send_embed(self, ctx, channel: discord.TextChannel, *, embed_string: str):
|
|
"""
|
|
Send a Discord embed from the embed builder to a specified channel.
|
|
Usage: !send_embed <channel> <embed_string>
|
|
"""
|
|
# Decode the embed string
|
|
try:
|
|
async with ctx.typing():
|
|
raw = base64.urlsafe_b64decode(embed_string.encode())
|
|
try:
|
|
raw = zlib.decompress(raw)
|
|
except zlib.error:
|
|
pass # Legacy uncompressed string
|
|
embed_data = json.loads(raw.decode())
|
|
|
|
# Ensure embed_data is a list (for embed chains)
|
|
if not isinstance(embed_data, list):
|
|
embed_data = [embed_data]
|
|
|
|
# Convert embed data to Discord embeds
|
|
embeds = await self._build_embeds(embed_data)
|
|
|
|
if not embeds:
|
|
await ctx.send("❌ No valid embeds found in the provided data.")
|
|
return
|
|
|
|
except json.JSONDecodeError as e:
|
|
await ctx.send(f"❌ Invalid embed string. Could not decode JSON: `{e}`")
|
|
return
|
|
except Exception as e:
|
|
await ctx.send(f"❌ Failed to build embeds: `{e}`")
|
|
return
|
|
|
|
# Confirm before sending
|
|
view = View(timeout=60)
|
|
confirmed = {"value": False}
|
|
|
|
async def yes_callback(interaction: discord.Interaction):
|
|
if interaction.user != ctx.author:
|
|
await interaction.response.send_message("You can't confirm this action.", ephemeral=True)
|
|
return
|
|
confirmed["value"] = True
|
|
await interaction.response.edit_message(content="✅ Confirmed. Sending embed...", view=None)
|
|
view.stop()
|
|
|
|
async def no_callback(interaction: discord.Interaction):
|
|
if interaction.user != ctx.author:
|
|
await interaction.response.send_message("You can't cancel this action.", ephemeral=True)
|
|
return
|
|
confirmed["value"] = False
|
|
await interaction.response.edit_message(content="❌ Cancelled.", view=None)
|
|
view.stop()
|
|
|
|
yes_button = Button(label="Yes", style=discord.ButtonStyle.green)
|
|
no_button = Button(label="No", style=discord.ButtonStyle.red)
|
|
yes_button.callback = yes_callback
|
|
no_button.callback = no_callback
|
|
view.add_item(yes_button)
|
|
view.add_item(no_button)
|
|
|
|
# Send preview
|
|
embed_count = len(embeds)
|
|
preview_text = f"Send {'this embed' if embed_count == 1 else f'{embed_count} embeds'} to {channel.mention}?"
|
|
|
|
try:
|
|
await ctx.send(
|
|
preview_text,
|
|
embeds=embeds[:10] if len(embeds) <= 10 else embeds[:1], # Preview max 10 embeds or just first one
|
|
view=view
|
|
)
|
|
|
|
if len(embeds) > 10:
|
|
await ctx.send(f"⚠️ Preview shows only the first embed. Total embeds to send: {embed_count}")
|
|
except Exception as e:
|
|
await ctx.send(f"❌ Failed to send preview: `{e}`")
|
|
return
|
|
|
|
await view.wait()
|
|
|
|
if not confirmed["value"]:
|
|
return
|
|
|
|
# Send the embeds to the target channel
|
|
try:
|
|
# Discord allows max 10 embeds per message
|
|
for i in range(0, len(embeds), 10):
|
|
chunk = embeds[i:i+10]
|
|
await channel.send(embeds=chunk)
|
|
|
|
await ctx.send(f"✅ Successfully sent {'embed' if embed_count == 1 else f'{embed_count} embeds'} to {channel.mention}")
|
|
|
|
except discord.Forbidden:
|
|
await ctx.send(f"❌ I don't have permission to send messages in {channel.mention}")
|
|
except Exception as e:
|
|
await ctx.send(f"❌ Failed to send embeds: `{e}`")
|
|
|
|
async def _build_embeds(self, embed_data: list) -> list[discord.Embed]:
|
|
"""Build Discord embed objects from embed data"""
|
|
embeds = []
|
|
for data in embed_data:
|
|
embed_type = data.get("type", "full")
|
|
|
|
# Handle image-only embeds
|
|
if embed_type == "image":
|
|
if data.get("image") and data["image"].get("url"):
|
|
embed = discord.Embed()
|
|
embed.set_image(url=data["image"]["url"])
|
|
embeds.append(embed)
|
|
continue
|
|
|
|
# Handle full embeds
|
|
embed = discord.Embed()
|
|
|
|
# Basic properties
|
|
if data.get("title"):
|
|
embed.title = data["title"]
|
|
if data.get("description"):
|
|
embed.description = data["description"]
|
|
if data.get("url"):
|
|
embed.url = data["url"]
|
|
if data.get("color"):
|
|
embed.color = data["color"]
|
|
|
|
# Author
|
|
if data.get("author"):
|
|
author = data["author"]
|
|
embed.set_author(
|
|
name=author.get("name", ""),
|
|
url=author.get("url"),
|
|
icon_url=author.get("icon_url")
|
|
)
|
|
|
|
# Footer
|
|
if data.get("footer"):
|
|
footer = data["footer"]
|
|
embed.set_footer(
|
|
text=footer.get("text", ""),
|
|
icon_url=footer.get("icon_url")
|
|
)
|
|
|
|
# Timestamp
|
|
if data.get("timestamp"):
|
|
# Discord.py will handle the timestamp if we set it
|
|
from datetime import datetime
|
|
embed.timestamp = datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00"))
|
|
|
|
# Fields
|
|
if data.get("fields"):
|
|
for field in data["fields"]:
|
|
if field.get("name") or field.get("value"):
|
|
embed.add_field(
|
|
name=field.get("name", "\u200b"),
|
|
value=field.get("value", "\u200b"),
|
|
inline=field.get("inline", False)
|
|
)
|
|
|
|
# Image
|
|
if data.get("image") and data["image"].get("url"):
|
|
embed.set_image(url=data["image"]["url"])
|
|
|
|
# Thumbnail
|
|
if data.get("thumbnail") and data["thumbnail"].get("url"):
|
|
embed.set_thumbnail(url=data["thumbnail"]["url"])
|
|
|
|
embeds.append(embed)
|
|
|
|
return embeds
|
|
|
|
|
|
async def setup(bot: commands.Bot):
|
|
await bot.add_cog(SendEmbedCommand(bot)) |