Enhance component message handling and introduce new features
- Added `ComponentMessageBinding` model to manage message components in the database. - Updated `WelcomeConfig`, `Tag`, and `ScheduledMessage` models to support new component fields. - Integrated component handling in various modules, including Scheduler and Tags, to improve message customization. - Enhanced user experience by implementing new commands for creating and managing component messages in the utility module. - Updated localization files to reflect new component-related features and improve user guidance.
This commit is contained in:
@@ -242,6 +242,14 @@ export const SETTINGS_SEARCH_FIELDS: SearchIndexEntry[] = [
|
||||
href: 'welcome',
|
||||
hash: 'field-welcome-embed'
|
||||
},
|
||||
{
|
||||
id: 'setting-welcome-components',
|
||||
kind: 'setting',
|
||||
labelKey: 'modulePages.welcome.welcomeComponents',
|
||||
href: 'welcome',
|
||||
hash: 'field-welcome-components',
|
||||
keywords: ['components v2', 'layout']
|
||||
},
|
||||
{
|
||||
id: 'setting-welcome-dm',
|
||||
kind: 'setting',
|
||||
@@ -292,6 +300,13 @@ export const SETTINGS_SEARCH_FIELDS: SearchIndexEntry[] = [
|
||||
href: 'welcome',
|
||||
hash: 'field-leave-channel'
|
||||
},
|
||||
{
|
||||
id: 'setting-leave-type',
|
||||
kind: 'setting',
|
||||
labelKey: 'modulePages.welcome.leaveType',
|
||||
href: 'welcome',
|
||||
hash: 'field-leave-type'
|
||||
},
|
||||
{
|
||||
id: 'setting-leave-content',
|
||||
kind: 'setting',
|
||||
@@ -306,6 +321,14 @@ export const SETTINGS_SEARCH_FIELDS: SearchIndexEntry[] = [
|
||||
href: 'welcome',
|
||||
hash: 'field-leave-embed'
|
||||
},
|
||||
{
|
||||
id: 'setting-leave-components',
|
||||
kind: 'setting',
|
||||
labelKey: 'modulePages.welcome.leaveComponents',
|
||||
href: 'welcome',
|
||||
hash: 'field-leave-components',
|
||||
keywords: ['components v2', 'layout']
|
||||
},
|
||||
// Verification
|
||||
{
|
||||
id: 'setting-verify-enabled',
|
||||
@@ -825,6 +848,15 @@ export const SETTINGS_SEARCH_FIELDS: SearchIndexEntry[] = [
|
||||
href: 'scheduler',
|
||||
hash: 'field-scheduler-create'
|
||||
},
|
||||
// Messages
|
||||
{
|
||||
id: 'setting-messages-send',
|
||||
kind: 'setting',
|
||||
labelKey: 'modulePages.messages.sendTitle',
|
||||
href: 'messages',
|
||||
hash: 'field-messages-send',
|
||||
keywords: ['send', 'post', 'embed', 'components v2']
|
||||
},
|
||||
// Backup
|
||||
{
|
||||
id: 'setting-backup',
|
||||
|
||||
73
apps/webui/src/lib/module-configs/messages.ts
Normal file
73
apps/webui/src/lib/module-configs/messages.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
DashboardMessageSendResultSchema,
|
||||
MessageComponentsV2Schema,
|
||||
type DashboardMessageSend,
|
||||
type DashboardMessageSendResult
|
||||
} from '@nexumi/shared';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import { addJobAndAwait, getMessagesQueue, getMessagesQueueEvents } from '../queues';
|
||||
|
||||
export class MessageSendTimeoutError extends Error {
|
||||
constructor() {
|
||||
super('The bot did not confirm the message in time. It may still have been sent — check the channel shortly.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a dashboard message via the bot's `messages` BullMQ queue
|
||||
* (`dashboardMessageSend` job). For Components V2, a binding row is created
|
||||
* first so interaction custom IDs can reference a stable ref id.
|
||||
*/
|
||||
export async function sendDashboardMessageFromWebui(
|
||||
guildId: string,
|
||||
createdById: string,
|
||||
input: DashboardMessageSend
|
||||
): Promise<DashboardMessageSendResult> {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
|
||||
let bindingId: string | null = null;
|
||||
if (input.mode === 'COMPONENTS_V2') {
|
||||
const components = MessageComponentsV2Schema.parse(input.components);
|
||||
const binding = await prisma.componentMessageBinding.create({
|
||||
data: {
|
||||
guildId,
|
||||
channelId: input.channelId,
|
||||
payload: components as Prisma.InputJsonValue,
|
||||
createdById
|
||||
}
|
||||
});
|
||||
bindingId = binding.id;
|
||||
}
|
||||
|
||||
const { confirmed, result } = await addJobAndAwait<{
|
||||
bindingId: string | null;
|
||||
messageId: string | null;
|
||||
channelId: string;
|
||||
}>(
|
||||
getMessagesQueue(),
|
||||
getMessagesQueueEvents(),
|
||||
'dashboardMessageSend',
|
||||
{
|
||||
guildId,
|
||||
channelId: input.channelId,
|
||||
createdById,
|
||||
mode: input.mode,
|
||||
embed: input.embed ?? null,
|
||||
components: input.components ?? null,
|
||||
bindingId
|
||||
},
|
||||
{ removeOnComplete: true, removeOnFail: 25 }
|
||||
);
|
||||
|
||||
if (!confirmed || !result) {
|
||||
throw new MessageSendTimeoutError();
|
||||
}
|
||||
|
||||
return DashboardMessageSendResultSchema.parse({
|
||||
bindingId: result.bindingId ?? bindingId,
|
||||
messageId: result.messageId,
|
||||
channelId: result.channelId,
|
||||
confirmed: true
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
MessageComponentsV2Schema,
|
||||
WelcomeEmbedSchema,
|
||||
type MessageComponentsV2,
|
||||
type ScheduledMessageDashboard,
|
||||
type ScheduledMessageDashboardCreate,
|
||||
type WelcomeEmbed
|
||||
@@ -19,6 +21,11 @@ function parseEmbed(raw: unknown): WelcomeEmbed | null {
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
function parseComponents(raw: unknown): MessageComponentsV2 | null {
|
||||
const parsed = MessageComponentsV2Schema.safeParse(raw);
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
function toDashboard(schedule: ScheduledMessage): ScheduledMessageDashboard {
|
||||
return {
|
||||
id: schedule.id,
|
||||
@@ -26,6 +33,7 @@ function toDashboard(schedule: ScheduledMessage): ScheduledMessageDashboard {
|
||||
creatorId: schedule.creatorId,
|
||||
content: schedule.content,
|
||||
embed: parseEmbed(schedule.embed),
|
||||
components: parseComponents(schedule.components),
|
||||
rolePingId: schedule.rolePingId,
|
||||
cron: schedule.cron,
|
||||
runAt: schedule.runAt?.toISOString() ?? null,
|
||||
@@ -84,6 +92,10 @@ export async function createScheduledMessageDashboard(
|
||||
input.embed === undefined || input.embed === null
|
||||
? Prisma.JsonNull
|
||||
: (input.embed as Prisma.InputJsonValue),
|
||||
components:
|
||||
input.components === undefined || input.components === null
|
||||
? Prisma.JsonNull
|
||||
: (input.components as Prisma.InputJsonValue),
|
||||
rolePingId: input.rolePingId || null,
|
||||
cron,
|
||||
runAt
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
MessageComponentsV2Schema,
|
||||
WelcomeEmbedSchema,
|
||||
type MessageComponentsV2,
|
||||
type TagDashboard,
|
||||
type TagDashboardCreate,
|
||||
type TagDashboardUpdate,
|
||||
@@ -26,12 +28,18 @@ function parseEmbed(raw: unknown): WelcomeEmbed | null {
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
function parseComponents(raw: unknown): MessageComponentsV2 | null {
|
||||
const parsed = MessageComponentsV2Schema.safeParse(raw);
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
function toDashboard(tag: Tag): TagDashboard {
|
||||
return {
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
content: tag.content,
|
||||
embed: parseEmbed(tag.embed),
|
||||
components: parseComponents(tag.components),
|
||||
responseType: tag.responseType as TagDashboard['responseType'],
|
||||
triggerWord: tag.triggerWord,
|
||||
allowedRoleIds: tag.allowedRoleIds,
|
||||
@@ -69,6 +77,10 @@ export async function createTag(
|
||||
input.embed === undefined || input.embed === null
|
||||
? Prisma.JsonNull
|
||||
: (input.embed as Prisma.InputJsonValue),
|
||||
components:
|
||||
input.components === undefined || input.components === null
|
||||
? Prisma.JsonNull
|
||||
: (input.components as Prisma.InputJsonValue),
|
||||
responseType: input.responseType,
|
||||
triggerWord: input.triggerWord ?? null,
|
||||
allowedRoleIds: input.allowedRoleIds,
|
||||
@@ -104,6 +116,12 @@ export async function updateTag(
|
||||
...(patch.embed !== undefined
|
||||
? { embed: patch.embed === null ? Prisma.JsonNull : (patch.embed as Prisma.InputJsonValue) }
|
||||
: {}),
|
||||
...(patch.components !== undefined
|
||||
? {
|
||||
components:
|
||||
patch.components === null ? Prisma.JsonNull : (patch.components as Prisma.InputJsonValue)
|
||||
}
|
||||
: {}),
|
||||
...(patch.responseType !== undefined ? { responseType: patch.responseType } : {}),
|
||||
...(patch.triggerWord !== undefined ? { triggerWord: patch.triggerWord } : {}),
|
||||
...(patch.allowedRoleIds !== undefined ? { allowedRoleIds: patch.allowedRoleIds } : {}),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
MessageComponentsV2Schema,
|
||||
WelcomeEmbedSchema,
|
||||
type MessageComponentsV2,
|
||||
type WelcomeConfigDashboard,
|
||||
type WelcomeConfigDashboardPatch,
|
||||
type WelcomeEmbed
|
||||
@@ -21,6 +23,11 @@ function parseEmbed(raw: unknown): WelcomeEmbed | null {
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
function parseComponents(raw: unknown): MessageComponentsV2 | null {
|
||||
const parsed = MessageComponentsV2Schema.safeParse(raw);
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
function toDashboard(config: Awaited<ReturnType<typeof ensureWelcomeConfig>>): WelcomeConfigDashboard {
|
||||
return {
|
||||
welcomeEnabled: config.welcomeEnabled,
|
||||
@@ -30,8 +37,11 @@ function toDashboard(config: Awaited<ReturnType<typeof ensureWelcomeConfig>>): W
|
||||
welcomeType: config.welcomeType as WelcomeConfigDashboard['welcomeType'],
|
||||
welcomeContent: config.welcomeContent,
|
||||
welcomeEmbed: parseEmbed(config.welcomeEmbed),
|
||||
welcomeComponents: parseComponents(config.welcomeComponents),
|
||||
leaveType: config.leaveType as WelcomeConfigDashboard['leaveType'],
|
||||
leaveContent: config.leaveContent,
|
||||
leaveEmbed: parseEmbed(config.leaveEmbed),
|
||||
leaveComponents: parseComponents(config.leaveComponents),
|
||||
welcomeDmEnabled: config.welcomeDmEnabled,
|
||||
welcomeDmContent: config.welcomeDmContent,
|
||||
userAutoroleIds: config.userAutoroleIds,
|
||||
@@ -52,7 +62,15 @@ export async function updateWelcomeDashboard(
|
||||
): Promise<WelcomeConfigDashboard> {
|
||||
await ensureWelcomeConfig(guildId);
|
||||
|
||||
const { welcomeChannelId, leaveChannelId, welcomeEmbed, leaveEmbed, ...rest } = patch;
|
||||
const {
|
||||
welcomeChannelId,
|
||||
leaveChannelId,
|
||||
welcomeEmbed,
|
||||
leaveEmbed,
|
||||
welcomeComponents,
|
||||
leaveComponents,
|
||||
...rest
|
||||
} = patch;
|
||||
|
||||
const data: Prisma.WelcomeConfigUpdateInput = { ...rest };
|
||||
if (welcomeChannelId !== undefined) {
|
||||
@@ -67,6 +85,14 @@ export async function updateWelcomeDashboard(
|
||||
if (leaveEmbed !== undefined) {
|
||||
data.leaveEmbed = leaveEmbed === null ? Prisma.JsonNull : (leaveEmbed as Prisma.InputJsonValue);
|
||||
}
|
||||
if (welcomeComponents !== undefined) {
|
||||
data.welcomeComponents =
|
||||
welcomeComponents === null ? Prisma.JsonNull : (welcomeComponents as Prisma.InputJsonValue);
|
||||
}
|
||||
if (leaveComponents !== undefined) {
|
||||
data.leaveComponents =
|
||||
leaveComponents === null ? Prisma.JsonNull : (leaveComponents as Prisma.InputJsonValue);
|
||||
}
|
||||
|
||||
const updated = await prisma.welcomeConfig.update({ where: { guildId }, data });
|
||||
return toDashboard(updated);
|
||||
|
||||
@@ -12,7 +12,8 @@ const REDIS_ONLY_MODULES = new Set<DashboardModuleId>([
|
||||
'selfroles',
|
||||
'feeds',
|
||||
'scheduler',
|
||||
'guildbackup'
|
||||
'guildbackup',
|
||||
'messages'
|
||||
]);
|
||||
|
||||
function redisModulesKey(guildId: string): string {
|
||||
@@ -121,7 +122,8 @@ export async function getModuleStatuses(guildId: string): Promise<ModuleStatus[]
|
||||
stats: statsConfig?.enabled ?? false,
|
||||
feeds: redisFlags.feeds ?? true,
|
||||
scheduler: redisFlags.scheduler ?? true,
|
||||
guildbackup: redisFlags.guildbackup ?? true
|
||||
guildbackup: redisFlags.guildbackup ?? true,
|
||||
messages: redisFlags.messages ?? true
|
||||
};
|
||||
|
||||
return DASHBOARD_MODULES.map((module) => ({
|
||||
|
||||
@@ -18,15 +18,18 @@ export const giveawayQueueName = 'giveaways';
|
||||
export const scheduleQueueName = 'schedules';
|
||||
export const guildBackupQueueName = 'guild-backups';
|
||||
export const suggestionsQueueName = 'suggestions';
|
||||
export const messagesQueueName = 'messages';
|
||||
|
||||
const globalForQueues = globalThis as unknown as {
|
||||
__nexumiGiveawayQueue?: Queue;
|
||||
__nexumiScheduleQueue?: Queue;
|
||||
__nexumiGuildBackupQueue?: Queue;
|
||||
__nexumiSuggestionsQueue?: Queue;
|
||||
__nexumiMessagesQueue?: Queue;
|
||||
__nexumiGiveawayQueueEvents?: QueueEvents;
|
||||
__nexumiGuildBackupQueueEvents?: QueueEvents;
|
||||
__nexumiSuggestionsQueueEvents?: QueueEvents;
|
||||
__nexumiMessagesQueueEvents?: QueueEvents;
|
||||
};
|
||||
|
||||
function queueConnection() {
|
||||
@@ -66,6 +69,13 @@ export function getSuggestionsQueue(): Queue {
|
||||
return globalForQueues.__nexumiSuggestionsQueue;
|
||||
}
|
||||
|
||||
export function getMessagesQueue(): Queue {
|
||||
globalForQueues.__nexumiMessagesQueue ??= new Queue(messagesQueueName, {
|
||||
connection: queueConnection()
|
||||
});
|
||||
return globalForQueues.__nexumiMessagesQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* QueueEvents instances are only needed for jobs where the dashboard needs
|
||||
* to wait for the bot to finish (e.g. ending a giveaway so the winners list
|
||||
@@ -92,6 +102,13 @@ export function getSuggestionsQueueEvents(): QueueEvents {
|
||||
return globalForQueues.__nexumiSuggestionsQueueEvents;
|
||||
}
|
||||
|
||||
export function getMessagesQueueEvents(): QueueEvents {
|
||||
globalForQueues.__nexumiMessagesQueueEvents ??= new QueueEvents(messagesQueueName, {
|
||||
connection: queueEventsConnection()
|
||||
});
|
||||
return globalForQueues.__nexumiMessagesQueueEvents;
|
||||
}
|
||||
|
||||
const DEFAULT_WAIT_TIMEOUT_MS = 15_000;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user