Files
SCNewsBot/scnewsbot/extensions/announcements.py
2026-03-14 23:09:38 -04:00

315 lines
11 KiB
Python

from __future__ import annotations
import discord
from discord.ext import commands
from datetime import datetime
from extensions import leaderboard
# CONFIG
DEFAULT_IMAGE_URL = "https://cdn.discordapp.com/attachments/611922107345141760/1348673800874754088/Polaris_over_Yela_bright.png"
LOGGING_CHANNEL_IDS = [1091876261938343986]
CHANNEL_OPTIONS = [
("Server News", 1113146864804573285),
("Community Feed", 1388237591845011516),
("SC News", 569635458183856149),
("General News", 803341100618219540),
("Patch Notes", 585952222853201941),
("Testing", 1062905729532571719),
]
PING_ROLE_OPTIONS = [
("Server News", 1113152142300156004),
("Community Feed", 1402452172595265596),
("SC News", 620025828079697920),
("General News", 803343410794594385),
("Patch Notes", 620025894559547412),
("Evocati Patch Notes", 1305975151858552862),
("MOTD", 1310721100392435797),
("Testing", 1473004437034242260),
]
LEADERBOARD_CHANNEL_IDS = {
1113146864804573285,
1388237591845011516,
569635458183856149,
803341100618219540,
585952222853201941,
}
EMBED_COLOR = discord.Color.blurple()
TIMEOUT = 900
# DATA MODEL
class Announcement:
def __init__(
self,
title: str = "",
description: str = "",
url: str | None = None,
image_url: str | None = None,
video_url: str | None = None,
channel: discord.TextChannel | None = None,
ping: discord.Role | None = None,
ping_preview: str | None = None,
publish: bool = False,
):
self.title = title
self.description = self.process_special_chars(description)
self.url = url
self.image_url = image_url or DEFAULT_IMAGE_URL
self.video_url = video_url
self.channel = channel
self.ping = ping
self.ping_preview = ping_preview
self.publish = publish
@classmethod
def from_message(cls, message: discord.Message):
embed = message.embeds[0]
return cls(
title=embed.title or "",
description=embed.description or "",
url=embed.url,
image_url=embed.image.url if embed.image else DEFAULT_IMAGE_URL,
channel=message.channel,
)
def embed(self) -> discord.Embed:
e = discord.Embed(
title=self.title or None,
description=self.description or None,
url=self.url,
color=EMBED_COLOR,
)
if self.image_url:
e.set_image(url=self.image_url)
return e
# ----------------- New helper -----------------
@staticmethod
def process_special_chars(text: str) -> str:
if not text:
return text
lines = text.splitlines()
processed_lines = []
for line in lines:
stripped = line.lstrip()
if stripped.startswith("-"):
processed_lines.append("" + stripped[1:].lstrip())
elif stripped.startswith("+"):
processed_lines.append("" + stripped[1:].lstrip())
else:
processed_lines.append(line)
return "\n".join(processed_lines)
# MODAL
class TextModal(discord.ui.Modal):
def __init__(self, builder, field: str, label: str, long: bool = False):
super().__init__(title=f"Edit {label}")
self.builder = builder
self.field = field
default_value = getattr(builder.announcement, field) or ""
self.input = discord.ui.TextInput(
label=label,
default=default_value,
style=discord.TextStyle.long if long else discord.TextStyle.short,
required=False,
)
self.add_item(self.input)
async def on_submit(self, interaction: discord.Interaction):
if self.field == "description":
setattr(
self.builder.announcement,
self.field,
Announcement.process_special_chars(self.input.value)
)
else:
setattr(self.builder.announcement, self.field, self.input.value)
self.builder.update_field_buttons()
await interaction.response.edit_message(
embed=self.builder.announcement.embed(),
view=self.builder.view,
)
# SELECTS
class ChannelSelect(discord.ui.Select):
def __init__(self, builder):
self.builder = builder
options = [discord.SelectOption(label=name, value=str(cid)) for name, cid in CHANNEL_OPTIONS]
super().__init__(placeholder="Select Channel", options=options, row=0)
async def callback(self, interaction: discord.Interaction):
cid = int(self.values[0])
self.builder.announcement.channel = interaction.guild.get_channel(cid)
self.placeholder = next((name for name, id in CHANNEL_OPTIONS if id == cid), "Select Channel")
await interaction.response.edit_message(embed=self.builder.announcement.embed(), view=self.builder.view)
class PingSelect(discord.ui.Select):
def __init__(self, builder):
self.builder = builder
options = [discord.SelectOption(label=name, value=str(rid)) for name, rid in PING_ROLE_OPTIONS]
super().__init__(placeholder="Select Ping Role", options=options, row=1)
async def callback(self, interaction: discord.Interaction):
rid = int(self.values[0])
self.builder.announcement.ping = interaction.guild.get_role(rid)
self.placeholder = next((name for name, id in PING_ROLE_OPTIONS if id == rid), "Select Ping Role")
await interaction.response.edit_message(embed=self.builder.announcement.embed(), view=self.builder.view)
# FIELD BUTTON
class FieldButton(discord.ui.Button):
def __init__(self, builder, field, label, row, style):
super().__init__(label=label, row=row, style=style)
self.builder = builder
self.field = field
async def callback(self, interaction: discord.Interaction):
await interaction.response.send_modal(
TextModal(self.builder, self.field, self.label, long=self.field=="description")
)
# CANCEL / POST / PUBLISH BUTTONS
class PublishButton(discord.ui.Button):
def __init__(self, builder):
label = "Published: ✅" if builder.announcement.publish else "Published: ❌"
super().__init__(label=label, style=discord.ButtonStyle.secondary, row=3)
self.builder = builder
async def callback(self, interaction: discord.Interaction):
self.builder.announcement.publish = not self.builder.announcement.publish
self.label = "Published: ✅" if self.builder.announcement.publish else "Published: ❌"
await interaction.response.edit_message(embed=self.builder.announcement.embed(), view=self.builder.view)
class CancelButton(discord.ui.Button):
def __init__(self, builder, editing=False):
super().__init__(label="Cancel", style=discord.ButtonStyle.danger, row=4)
self.builder = builder
self.editing = editing
async def callback(self, interaction: discord.Interaction):
await interaction.message.delete()
msg_text = "Announcement editing cancelled." if self.editing else "Announcement creation cancelled."
await interaction.response.send_message(msg_text, ephemeral=True)
self.view.stop()
class PostButton(discord.ui.Button):
def __init__(self, builder, editing=False):
label = "Edit" if editing else "Post"
super().__init__(label=label, style=discord.ButtonStyle.blurple, row=4)
self.builder = builder
self.editing = editing
async def callback(self, interaction: discord.Interaction):
ann = self.builder.announcement
if not ann.channel:
await interaction.response.send_message("Select a channel first.", ephemeral=True)
return
if self.editing and self.builder.target:
msg = self.builder.target
await msg.edit(embed=ann.embed())
else:
msg = await ann.channel.send(embed=ann.embed())
if ann.publish and isinstance(ann.channel, discord.TextChannel) and ann.channel.is_news():
await msg.publish()
if ann.video_url:
await ann.channel.send(ann.video_url)
ping_msg = ""
if ann.ping:
ping_msg += f"{ann.ping.mention}"
if ann.ping_preview:
ping_msg += f" - {ann.ping_preview}"
if ping_msg:
await ann.channel.send(ping_msg)
for cid in LOGGING_CHANNEL_IDS:
ch = self.builder.ctx.guild.get_channel(cid)
if ch:
await ch.send(embed=ann.embed())
if ann.channel.id in LEADERBOARD_CHANNEL_IDS:
leaderboard.record_announcement_post(interaction.user.id)
await interaction.response.send_message(
"The announcement has been posted! \nhttps://i.postimg.cc/J48Vk8my/meme-8-1.gif"
if not self.editing else "Announcement edited!",
ephemeral=False
)
self.view.stop()
# VIEW
class BuilderView(discord.ui.View):
def __init__(self, builder, editing=False):
super().__init__(timeout=TIMEOUT)
self.builder = builder
self.editing = editing
self.add_item(ChannelSelect(builder))
self.add_item(PingSelect(builder))
self.update_field_buttons()
self.add_item(PublishButton(builder))
self.add_item(CancelButton(builder, editing=editing))
self.add_item(PostButton(builder, editing=editing))
def update_field_buttons(self):
field_buttons = [item for item in self.children if isinstance(item, FieldButton)]
for btn in field_buttons:
self.remove_item(btn)
for field, label, row, long in [
("title", "Title", 2, False),
("description", "Description", 2, True),
("url", "URL", 2, False),
("image_url", "Image", 3, False),
("video_url", "Video", 3, False),
("ping_preview", "Ping Preview", 3, False),
]:
style = discord.ButtonStyle.success if getattr(self.builder.announcement, field) else discord.ButtonStyle.gray
self.add_item(FieldButton(self.builder, field, label, row, style))
# BUILDER
class Builder:
def __init__(self, ctx, announcement, target=None, editing=False):
self.ctx = ctx
self.announcement = announcement
self.target = target
self.editing = editing
self.view = BuilderView(self, editing=editing)
def update_field_buttons(self):
self.view.update_field_buttons()
async def start(self):
await self.ctx.send(embed=self.announcement.embed(), view=self.view)
# COG
class Announcements(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.group(invoke_without_command=True)
async def embed(self, ctx):
await ctx.send_help(ctx.command)
@embed.command()
async def create(self, ctx):
await Builder(ctx, Announcement(channel=ctx.channel)).start()
@embed.command()
async def edit(self, ctx, message: discord.Message):
if not message.embeds:
await ctx.reply("That message has no embed.")
return
await Builder(ctx, Announcement.from_message(message), target=message, editing=True).start()
# SETUP
async def setup(bot: commands.Bot):
await bot.add_cog(Announcements(bot))