1
0
Fork 0

feat(bot): pick the target channel inside the announce modal

- The compose modal gains an optional channel select (text and
  announcement channels), preselected with the configured default
- /announce now posts only to the announce channel by default: the ops
  fallback is gone, so an unconfigured announce channel errors instead
  of silently posting to ops
- Roll call was already ops-only and is unchanged

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-09-10 23:58:03 -04:00
parent 0bda8c56e3
commit ecdbcbd15b

View file

@ -1,7 +1,9 @@
import { import {
ActionRowBuilder, ChannelSelectMenuBuilder,
ChannelType,
ComponentType, ComponentType,
EmbedBuilder, EmbedBuilder,
LabelBuilder,
MessageFlags, MessageFlags,
ModalBuilder, ModalBuilder,
SlashCommandBuilder, SlashCommandBuilder,
@ -20,6 +22,7 @@ const ANNOUNCE_COLOR = 0xf59e0b;
const ANNOUNCE_MODAL_PREFIX = "ptf-announce:"; const ANNOUNCE_MODAL_PREFIX = "ptf-announce:";
const TITLE_INPUT_ID = "ptf-announce-title"; const TITLE_INPUT_ID = "ptf-announce-title";
const BODY_INPUT_ID = "ptf-announce-body"; const BODY_INPUT_ID = "ptf-announce-body";
const CHANNEL_INPUT_ID = "ptf-announce-channel";
/** Modal paragraph inputs cap at 4000 chars; the embed description limit is 4096. */ /** Modal paragraph inputs cap at 4000 chars; the embed description limit is 4096. */
const MAX_BODY_LENGTH = 4000; const MAX_BODY_LENGTH = 4000;
@ -33,6 +36,12 @@ const modalText = (fields: ModalSubmitInteraction["fields"], customId: string):
return field?.type === ComponentType.TextInput ? field.value : ""; return field?.type === ComponentType.TextInput ? field.value : "";
}; };
const modalChannelId = (fields: ModalSubmitInteraction["fields"]): string | null => {
const field = fields.fields.get(CHANNEL_INPUT_ID);
if (field?.type !== ComponentType.ChannelSelect) return null;
return field.channels?.first()?.id ?? null;
};
const postAnnouncement = async ( const postAnnouncement = async (
channel: TextBasedChannelFields, channel: TextBasedChannelFields,
title: string, title: string,
@ -75,33 +84,33 @@ export const announceCommand: BotCommand = {
const message = (interaction.options.getString("message") ?? "").trim(); const message = (interaction.options.getString("message") ?? "").trim();
const option = interaction.options.getChannel("channel"); const option = interaction.options.getChannel("channel");
const channelId = option?.id ?? botConfig.announceChannelId ?? botConfig.opsChannelId; const channelId = option?.id ?? botConfig.announceChannelId;
if (!channelId) { if (!channelId) {
await interaction.reply( await interaction.reply(
ephemeral("No announce channel is configured and no channel was given."), ephemeral(
"No announce channel is configured (set DISCORD_ANNOUNCE_CHANNEL_ID) and no channel was given.",
),
); );
return; return;
} }
if (!message) { if (!message) {
// Compose in a modal. The resolved channel rides along in the custom id, // customId carries the default channel as the fallback: modal submits
// because modal submit interactions do not carry the command's options. // don't carry the command's options.
const modal = new ModalBuilder() const modal = new ModalBuilder()
.setCustomId(`${ANNOUNCE_MODAL_PREFIX}${channelId}`) .setCustomId(`${ANNOUNCE_MODAL_PREFIX}${channelId}`)
.setTitle("Post an announcement"); .setTitle("Post an announcement");
modal.addComponents( modal.addLabelComponents(
new ActionRowBuilder<TextInputBuilder>().addComponents( new LabelBuilder().setLabel("Title (optional)").setTextInputComponent(
new TextInputBuilder() new TextInputBuilder()
.setCustomId(TITLE_INPUT_ID) .setCustomId(TITLE_INPUT_ID)
.setLabel("Title (optional)")
.setStyle(TextInputStyle.Short) .setStyle(TextInputStyle.Short)
.setMaxLength(256) .setMaxLength(256)
.setRequired(false), .setRequired(false),
), ),
new ActionRowBuilder<TextInputBuilder>().addComponents( new LabelBuilder().setLabel("Announcement text").setTextInputComponent(
new TextInputBuilder() new TextInputBuilder()
.setCustomId(BODY_INPUT_ID) .setCustomId(BODY_INPUT_ID)
.setLabel("Announcement text")
.setStyle(TextInputStyle.Paragraph) .setStyle(TextInputStyle.Paragraph)
.setMaxLength(MAX_BODY_LENGTH) .setMaxLength(MAX_BODY_LENGTH)
.setRequired(true) .setRequired(true)
@ -109,6 +118,16 @@ export const announceCommand: BotCommand = {
"Paste your announcement. Markdown (headings, lists, bold, code) is supported.", "Paste your announcement. Markdown (headings, lists, bold, code) is supported.",
), ),
), ),
new LabelBuilder()
.setLabel("Channel (optional)")
.setDescription("Defaults to the configured app updates channel.")
.setChannelSelectMenuComponent(
new ChannelSelectMenuBuilder()
.setCustomId(CHANNEL_INPUT_ID)
.addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement)
.setDefaultChannels(channelId)
.setRequired(false),
),
); );
await interaction.showModal(modal); await interaction.showModal(modal);
return; return;
@ -145,9 +164,11 @@ export const handleAnnounceModal = async (
} }
// The modal custom id is "ptf-announce:<channelId>", written when the modal // The modal custom id is "ptf-announce:<channelId>", written when the modal
// was opened from the slash command. Fall back to the configured channel. // was opened. A channel picked in the modal overrides it.
const customChannelId = interaction.customId.slice(ANNOUNCE_MODAL_PREFIX.length); const customChannelId = interaction.customId.slice(ANNOUNCE_MODAL_PREFIX.length);
const resolvedId = customChannelId || botConfig.announceChannelId || botConfig.opsChannelId; const resolvedId =
modalChannelId(interaction.fields) ??
(customChannelId || botConfig.announceChannelId);
if (!resolvedId) { if (!resolvedId) {
await interaction.reply(ephemeral("No announce channel is configured.")); await interaction.reply(ephemeral("No announce channel is configured."));
return; return;