Refactor giveaway job processing and enhance environment documentation
- Improved handling of the `giveawayCreate` job in the bot's job processing for better consistency. - Updated `.env.example` to clarify the usage of `BOT_TOKEN` for both the bot and WebUI. - Added `bullmq` dependency to the WebUI package for enhanced job management capabilities.
This commit is contained in:
65
apps/webui/src/lib/module-configs/feeds.ts
Normal file
65
apps/webui/src/lib/module-configs/feeds.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { SocialFeedDashboard, SocialFeedDashboardCreate, SocialFeedDashboardUpdate } from '@nexumi/shared';
|
||||
import type { SocialFeed } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
|
||||
function toDashboard(feed: SocialFeed): SocialFeedDashboard {
|
||||
return {
|
||||
id: feed.id,
|
||||
type: feed.type as SocialFeedDashboard['type'],
|
||||
sourceId: feed.sourceId,
|
||||
channelId: feed.channelId,
|
||||
rolePingId: feed.rolePingId ?? '',
|
||||
template: feed.template,
|
||||
enabled: feed.enabled
|
||||
};
|
||||
}
|
||||
|
||||
export async function listSocialFeeds(guildId: string): Promise<SocialFeedDashboard[]> {
|
||||
const feeds = await prisma.socialFeed.findMany({ where: { guildId }, orderBy: { createdAt: 'asc' } });
|
||||
return feeds.map(toDashboard);
|
||||
}
|
||||
|
||||
export async function createSocialFeed(
|
||||
guildId: string,
|
||||
input: SocialFeedDashboardCreate
|
||||
): Promise<SocialFeedDashboard> {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
const feed = await prisma.socialFeed.create({
|
||||
data: {
|
||||
guildId,
|
||||
type: input.type,
|
||||
sourceId: input.sourceId,
|
||||
channelId: input.channelId,
|
||||
rolePingId: input.rolePingId ? input.rolePingId : null,
|
||||
template: input.template ?? null,
|
||||
enabled: input.enabled
|
||||
}
|
||||
});
|
||||
return toDashboard(feed);
|
||||
}
|
||||
|
||||
export async function updateSocialFeed(
|
||||
guildId: string,
|
||||
feedId: string,
|
||||
patch: SocialFeedDashboardUpdate
|
||||
): Promise<SocialFeedDashboard | null> {
|
||||
const existing = await prisma.socialFeed.findFirst({ where: { id: feedId, guildId } });
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { rolePingId, ...rest } = patch;
|
||||
const updated = await prisma.socialFeed.update({
|
||||
where: { id: feedId },
|
||||
data: {
|
||||
...rest,
|
||||
...(rolePingId !== undefined ? { rolePingId: rolePingId === '' ? null : rolePingId } : {})
|
||||
}
|
||||
});
|
||||
return toDashboard(updated);
|
||||
}
|
||||
|
||||
export async function deleteSocialFeed(guildId: string, feedId: string): Promise<boolean> {
|
||||
const result = await prisma.socialFeed.deleteMany({ where: { id: feedId, guildId } });
|
||||
return result.count > 0;
|
||||
}
|
||||
141
apps/webui/src/lib/module-configs/giveaways.ts
Normal file
141
apps/webui/src/lib/module-configs/giveaways.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { GiveawayCreateDashboard, GiveawayDashboard } from '@nexumi/shared';
|
||||
import type { Giveaway } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import { addJobAndAwait, giveawayQueue, giveawayQueueEvents } from '../queues';
|
||||
|
||||
export class GiveawayCreateTimeoutError extends Error {
|
||||
constructor() {
|
||||
super('The bot did not confirm the giveaway in time. It may still be starting - check the list shortly.');
|
||||
}
|
||||
}
|
||||
|
||||
function toDashboard(giveaway: Giveaway): GiveawayDashboard {
|
||||
return {
|
||||
id: giveaway.id,
|
||||
guildId: giveaway.guildId,
|
||||
channelId: giveaway.channelId,
|
||||
messageId: giveaway.messageId,
|
||||
prize: giveaway.prize,
|
||||
winnerCount: giveaway.winnerCount,
|
||||
endsAt: giveaway.endsAt.toISOString(),
|
||||
requiredRoleId: giveaway.requiredRoleId,
|
||||
requiredLevel: giveaway.requiredLevel,
|
||||
requiredMemberDays: giveaway.requiredMemberDays,
|
||||
paused: giveaway.paused,
|
||||
endedAt: giveaway.endedAt?.toISOString() ?? null,
|
||||
winners: giveaway.winners,
|
||||
entrants: giveaway.entrants,
|
||||
hostId: giveaway.hostId,
|
||||
createdAt: giveaway.createdAt.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export async function listGiveaways(guildId: string): Promise<GiveawayDashboard[]> {
|
||||
const giveaways = await prisma.giveaway.findMany({
|
||||
where: { guildId },
|
||||
orderBy: [{ endedAt: 'asc' }, { endsAt: 'asc' }],
|
||||
take: 50
|
||||
});
|
||||
return giveaways.map(toDashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* The WebUI has no discord.js Client to post the giveaway announcement
|
||||
* message, so creation is delegated to the bot via the same `giveaways`
|
||||
* BullMQ queue used by `/giveaway start` (see
|
||||
* `apps/bot/src/modules/giveaways/service.ts#runGiveawayCreateJob`). We wait
|
||||
* (bounded) for the job to finish so the dashboard can show the created
|
||||
* giveaway immediately.
|
||||
*/
|
||||
export async function createGiveawayDashboard(
|
||||
guildId: string,
|
||||
hostId: string,
|
||||
input: GiveawayCreateDashboard
|
||||
): Promise<GiveawayDashboard> {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
|
||||
const durationMs = new Date(input.endsAt).getTime() - Date.now();
|
||||
if (durationMs <= 0) {
|
||||
throw new GiveawayCreateTimeoutError();
|
||||
}
|
||||
|
||||
const { confirmed, result } = await addJobAndAwait<{ id: string }>(
|
||||
giveawayQueue,
|
||||
giveawayQueueEvents,
|
||||
'giveawayCreate',
|
||||
{
|
||||
guildId,
|
||||
channelId: input.channelId,
|
||||
hostId,
|
||||
prize: input.prize,
|
||||
winnerCount: input.winnerCount,
|
||||
durationMs,
|
||||
requiredRoleId: input.requiredRoleId || null,
|
||||
requiredLevel: input.requiredLevel ?? null,
|
||||
requiredMemberDays: input.requiredMemberDays ?? null
|
||||
},
|
||||
{ removeOnComplete: true, removeOnFail: 25 }
|
||||
);
|
||||
|
||||
if (!confirmed || !result) {
|
||||
throw new GiveawayCreateTimeoutError();
|
||||
}
|
||||
|
||||
const giveaway = await prisma.giveaway.findUnique({ where: { id: result.id } });
|
||||
if (!giveaway) {
|
||||
throw new GiveawayCreateTimeoutError();
|
||||
}
|
||||
return toDashboard(giveaway);
|
||||
}
|
||||
|
||||
export async function setGiveawayPaused(
|
||||
guildId: string,
|
||||
giveawayId: string,
|
||||
paused: boolean
|
||||
): Promise<GiveawayDashboard | null> {
|
||||
const existing = await prisma.giveaway.findFirst({ where: { id: giveawayId, guildId } });
|
||||
if (!existing || existing.endedAt) {
|
||||
return null;
|
||||
}
|
||||
const updated = await prisma.giveaway.update({ where: { id: giveawayId }, data: { paused } });
|
||||
return toDashboard(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends a giveaway immediately by re-scheduling its `giveawayEnd` job with no
|
||||
* delay and waiting for the bot to draw winners, update the Discord message
|
||||
* and DM them - see `apps/bot/src/modules/giveaways/service.ts#endGiveaway`.
|
||||
*/
|
||||
export async function endGiveawayDashboard(guildId: string, giveawayId: string): Promise<GiveawayDashboard | null> {
|
||||
const existing = await prisma.giveaway.findFirst({ where: { id: giveawayId, guildId } });
|
||||
if (!existing || existing.endedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingJob = await giveawayQueue.getJob(`giveaway-end-${giveawayId}`);
|
||||
await existingJob?.remove();
|
||||
|
||||
await addJobAndAwait(
|
||||
giveawayQueue,
|
||||
giveawayQueueEvents,
|
||||
'giveawayEnd',
|
||||
{ giveawayId },
|
||||
{ jobId: `giveaway-end-${giveawayId}`, removeOnComplete: true, removeOnFail: 50, delay: 0 }
|
||||
);
|
||||
|
||||
const updated = await prisma.giveaway.findUnique({ where: { id: giveawayId } });
|
||||
return updated ? toDashboard(updated) : null;
|
||||
}
|
||||
|
||||
export async function deleteGiveawayDashboard(guildId: string, giveawayId: string): Promise<boolean> {
|
||||
const existing = await prisma.giveaway.findFirst({ where: { id: giveawayId, guildId } });
|
||||
if (!existing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const job = await giveawayQueue.getJob(`giveaway-end-${giveawayId}`);
|
||||
await job?.remove();
|
||||
|
||||
await prisma.giveaway.delete({ where: { id: giveawayId } });
|
||||
return true;
|
||||
}
|
||||
111
apps/webui/src/lib/module-configs/scheduler.ts
Normal file
111
apps/webui/src/lib/module-configs/scheduler.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { ScheduledMessageDashboard, ScheduledMessageDashboardCreate } from '@nexumi/shared';
|
||||
import type { ScheduledMessage } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import { scheduleQueue } from '../queues';
|
||||
|
||||
export class SchedulerValidationError extends Error {}
|
||||
|
||||
function isValidCron(pattern: string): boolean {
|
||||
const parts = pattern.trim().split(/\s+/);
|
||||
return parts.length >= 5 && parts.length <= 6;
|
||||
}
|
||||
|
||||
function toDashboard(schedule: ScheduledMessage): ScheduledMessageDashboard {
|
||||
return {
|
||||
id: schedule.id,
|
||||
channelId: schedule.channelId,
|
||||
creatorId: schedule.creatorId,
|
||||
content: schedule.content,
|
||||
rolePingId: schedule.rolePingId,
|
||||
cron: schedule.cron,
|
||||
runAt: schedule.runAt?.toISOString() ?? null,
|
||||
enabled: schedule.enabled,
|
||||
createdAt: schedule.createdAt.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export async function listSchedules(guildId: string): Promise<ScheduledMessageDashboard[]> {
|
||||
const schedules = await prisma.scheduledMessage.findMany({
|
||||
where: { guildId, enabled: true },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
return schedules.map(toDashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scheduled message and enqueues the exact `scheduleSend` BullMQ
|
||||
* job the bot's `/schedule create` command would enqueue (same queue, job
|
||||
* name and delay/repeat semantics) - see
|
||||
* `apps/bot/src/modules/scheduler/service.ts#enqueueScheduleJob`. The bot's
|
||||
* existing `scheduleQueueName` worker sends the message, so no bot-side
|
||||
* changes are required.
|
||||
*/
|
||||
export async function createSchedule(
|
||||
guildId: string,
|
||||
creatorId: string,
|
||||
input: ScheduledMessageDashboardCreate
|
||||
): Promise<ScheduledMessageDashboard> {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
|
||||
const cron = input.cron?.trim() || null;
|
||||
if (cron && !isValidCron(cron)) {
|
||||
throw new SchedulerValidationError('Invalid cron expression (expects 5-6 space separated fields)');
|
||||
}
|
||||
|
||||
const runAt = !cron && input.runAt ? new Date(input.runAt) : null;
|
||||
if (!cron && runAt && runAt.getTime() <= Date.now()) {
|
||||
throw new SchedulerValidationError('runAt must be in the future');
|
||||
}
|
||||
|
||||
const schedule = await prisma.scheduledMessage.create({
|
||||
data: {
|
||||
guildId,
|
||||
channelId: input.channelId,
|
||||
creatorId,
|
||||
content: input.content?.trim() || null,
|
||||
rolePingId: input.rolePingId ? input.rolePingId : null,
|
||||
cron,
|
||||
runAt
|
||||
}
|
||||
});
|
||||
|
||||
const jobOptions: {
|
||||
jobId: string;
|
||||
removeOnComplete: boolean;
|
||||
removeOnFail: number;
|
||||
delay?: number;
|
||||
repeat?: { pattern: string };
|
||||
} = {
|
||||
jobId: `schedule-${schedule.id}`,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 50
|
||||
};
|
||||
if (cron) {
|
||||
jobOptions.repeat = { pattern: cron };
|
||||
} else if (runAt) {
|
||||
jobOptions.delay = Math.max(0, runAt.getTime() - Date.now());
|
||||
}
|
||||
|
||||
const job = await scheduleQueue.add('scheduleSend', { scheduleId: schedule.id }, jobOptions);
|
||||
const updated = await prisma.scheduledMessage.update({
|
||||
where: { id: schedule.id },
|
||||
data: { jobId: String(job.id) }
|
||||
});
|
||||
|
||||
return toDashboard(updated);
|
||||
}
|
||||
|
||||
export async function deleteSchedule(guildId: string, scheduleId: string): Promise<boolean> {
|
||||
const schedule = await prisma.scheduledMessage.findFirst({ where: { id: scheduleId, guildId } });
|
||||
if (!schedule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (schedule.jobId) {
|
||||
const job = await scheduleQueue.getJob(schedule.jobId);
|
||||
await job?.remove();
|
||||
}
|
||||
|
||||
await prisma.scheduledMessage.delete({ where: { id: scheduleId } });
|
||||
return true;
|
||||
}
|
||||
91
apps/webui/src/lib/module-configs/selfroles.ts
Normal file
91
apps/webui/src/lib/module-configs/selfroles.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type {
|
||||
SelfRoleEntry,
|
||||
SelfRolePanelDashboard,
|
||||
SelfRolePanelDashboardCreate,
|
||||
SelfRolePanelDashboardUpdate
|
||||
} from '@nexumi/shared';
|
||||
import type { Prisma, SelfRolePanel } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
|
||||
function toRolesArray(raw: unknown): SelfRoleEntry[] {
|
||||
if (!Array.isArray(raw)) {
|
||||
return [];
|
||||
}
|
||||
return raw.filter(
|
||||
(entry): entry is SelfRoleEntry =>
|
||||
Boolean(entry) && typeof entry === 'object' && typeof (entry as SelfRoleEntry).roleId === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
function toDashboard(panel: SelfRolePanel): SelfRolePanelDashboard {
|
||||
return {
|
||||
id: panel.id,
|
||||
channelId: panel.channelId,
|
||||
title: panel.title,
|
||||
description: panel.description,
|
||||
mode: panel.mode as SelfRolePanelDashboard['mode'],
|
||||
behavior: panel.behavior as SelfRolePanelDashboard['behavior'],
|
||||
roles: toRolesArray(panel.roles),
|
||||
expiresAt: panel.expiresAt?.toISOString() ?? null
|
||||
};
|
||||
}
|
||||
|
||||
export async function listSelfRolePanels(guildId: string): Promise<SelfRolePanelDashboard[]> {
|
||||
const panels = await prisma.selfRolePanel.findMany({
|
||||
where: { guildId },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
return panels.map(toDashboard);
|
||||
}
|
||||
|
||||
export async function createSelfRolePanel(
|
||||
guildId: string,
|
||||
input: SelfRolePanelDashboardCreate
|
||||
): Promise<SelfRolePanelDashboard> {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
const panel = await prisma.selfRolePanel.create({
|
||||
data: {
|
||||
guildId,
|
||||
channelId: input.channelId,
|
||||
title: input.title,
|
||||
description: input.description ?? null,
|
||||
mode: input.mode,
|
||||
behavior: input.behavior,
|
||||
roles: input.roles as unknown as Prisma.InputJsonValue,
|
||||
expiresAt: input.expiresAt ? new Date(input.expiresAt) : null
|
||||
}
|
||||
});
|
||||
return toDashboard(panel);
|
||||
}
|
||||
|
||||
export async function updateSelfRolePanel(
|
||||
guildId: string,
|
||||
panelId: string,
|
||||
patch: SelfRolePanelDashboardUpdate
|
||||
): Promise<SelfRolePanelDashboard | null> {
|
||||
const existing = await prisma.selfRolePanel.findFirst({ where: { id: panelId, guildId } });
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updated = await prisma.selfRolePanel.update({
|
||||
where: { id: panelId },
|
||||
data: {
|
||||
...(patch.channelId !== undefined ? { channelId: patch.channelId } : {}),
|
||||
...(patch.title !== undefined ? { title: patch.title } : {}),
|
||||
...(patch.description !== undefined ? { description: patch.description } : {}),
|
||||
...(patch.mode !== undefined ? { mode: patch.mode } : {}),
|
||||
...(patch.behavior !== undefined ? { behavior: patch.behavior } : {}),
|
||||
...(patch.roles !== undefined ? { roles: patch.roles as unknown as Prisma.InputJsonValue } : {}),
|
||||
...(patch.expiresAt !== undefined
|
||||
? { expiresAt: patch.expiresAt ? new Date(patch.expiresAt) : null }
|
||||
: {})
|
||||
}
|
||||
});
|
||||
return toDashboard(updated);
|
||||
}
|
||||
|
||||
export async function deleteSelfRolePanel(guildId: string, panelId: string): Promise<boolean> {
|
||||
const result = await prisma.selfRolePanel.deleteMany({ where: { id: panelId, guildId } });
|
||||
return result.count > 0;
|
||||
}
|
||||
99
apps/webui/src/lib/module-configs/suggestions.ts
Normal file
99
apps/webui/src/lib/module-configs/suggestions.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type {
|
||||
SuggestionAction,
|
||||
SuggestionConfigDashboard,
|
||||
SuggestionConfigDashboardPatch,
|
||||
SuggestionDashboard,
|
||||
SuggestionListQuery
|
||||
} from '@nexumi/shared';
|
||||
import type { Suggestion } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
|
||||
async function ensureSuggestionConfig(guildId: string) {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
return prisma.suggestionConfig.upsert({ where: { guildId }, update: {}, create: { guildId } });
|
||||
}
|
||||
|
||||
function toConfigDashboard(
|
||||
config: Awaited<ReturnType<typeof ensureSuggestionConfig>>
|
||||
): SuggestionConfigDashboard {
|
||||
return {
|
||||
enabled: config.enabled,
|
||||
openChannelId: config.openChannelId ?? '',
|
||||
approvedChannelId: config.approvedChannelId ?? '',
|
||||
deniedChannelId: config.deniedChannelId ?? '',
|
||||
createThread: config.createThread
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSuggestionConfigDashboard(guildId: string): Promise<SuggestionConfigDashboard> {
|
||||
const config = await ensureSuggestionConfig(guildId);
|
||||
return toConfigDashboard(config);
|
||||
}
|
||||
|
||||
export async function updateSuggestionConfigDashboard(
|
||||
guildId: string,
|
||||
patch: SuggestionConfigDashboardPatch
|
||||
): Promise<SuggestionConfigDashboard> {
|
||||
await ensureSuggestionConfig(guildId);
|
||||
|
||||
const { openChannelId, approvedChannelId, deniedChannelId, ...rest } = patch;
|
||||
const updated = await prisma.suggestionConfig.update({
|
||||
where: { guildId },
|
||||
data: {
|
||||
...rest,
|
||||
...(openChannelId !== undefined ? { openChannelId: openChannelId === '' ? null : openChannelId } : {}),
|
||||
...(approvedChannelId !== undefined
|
||||
? { approvedChannelId: approvedChannelId === '' ? null : approvedChannelId }
|
||||
: {}),
|
||||
...(deniedChannelId !== undefined
|
||||
? { deniedChannelId: deniedChannelId === '' ? null : deniedChannelId }
|
||||
: {})
|
||||
}
|
||||
});
|
||||
return toConfigDashboard(updated);
|
||||
}
|
||||
|
||||
function toSuggestionDashboard(suggestion: Suggestion): SuggestionDashboard {
|
||||
return {
|
||||
id: suggestion.id,
|
||||
authorId: suggestion.authorId,
|
||||
content: suggestion.content,
|
||||
status: suggestion.status as SuggestionDashboard['status'],
|
||||
upvotes: suggestion.upvotes,
|
||||
downvotes: suggestion.downvotes,
|
||||
staffReason: suggestion.staffReason,
|
||||
createdAt: suggestion.createdAt.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export async function listSuggestions(
|
||||
guildId: string,
|
||||
query: SuggestionListQuery
|
||||
): Promise<SuggestionDashboard[]> {
|
||||
const suggestions = await prisma.suggestion.findMany({
|
||||
where: { guildId, ...(query.status ? { status: query.status } : {}) },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: query.limit
|
||||
});
|
||||
return suggestions.map(toSuggestionDashboard);
|
||||
}
|
||||
|
||||
export async function applySuggestionAction(
|
||||
guildId: string,
|
||||
suggestionId: string,
|
||||
action: SuggestionAction
|
||||
): Promise<SuggestionDashboard | null> {
|
||||
const existing = await prisma.suggestion.findFirst({ where: { id: suggestionId, guildId } });
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updated = await prisma.suggestion.update({
|
||||
where: { id: suggestionId },
|
||||
data: {
|
||||
status: action.status,
|
||||
staffReason: action.staffReason ?? null
|
||||
}
|
||||
});
|
||||
return toSuggestionDashboard(updated);
|
||||
}
|
||||
90
apps/webui/src/lib/module-configs/tags.ts
Normal file
90
apps/webui/src/lib/module-configs/tags.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { TagDashboard, TagDashboardCreate, TagDashboardUpdate } from '@nexumi/shared';
|
||||
import type { Tag } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
|
||||
export class TagConflictError extends Error {
|
||||
constructor() {
|
||||
super('A tag with this name already exists');
|
||||
}
|
||||
}
|
||||
|
||||
function toDashboard(tag: Tag): TagDashboard {
|
||||
return {
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
content: tag.content,
|
||||
responseType: tag.responseType as TagDashboard['responseType'],
|
||||
triggerWord: tag.triggerWord,
|
||||
allowedRoleIds: tag.allowedRoleIds,
|
||||
allowedChannelIds: tag.allowedChannelIds
|
||||
};
|
||||
}
|
||||
|
||||
export async function listTags(guildId: string): Promise<TagDashboard[]> {
|
||||
const tags = await prisma.tag.findMany({ where: { guildId }, orderBy: { name: 'asc' } });
|
||||
return tags.map(toDashboard);
|
||||
}
|
||||
|
||||
export async function createTag(
|
||||
guildId: string,
|
||||
createdById: string,
|
||||
input: TagDashboardCreate
|
||||
): Promise<TagDashboard> {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
try {
|
||||
const tag = await prisma.tag.create({
|
||||
data: {
|
||||
guildId,
|
||||
name: input.name,
|
||||
content: input.content ?? null,
|
||||
responseType: input.responseType,
|
||||
triggerWord: input.triggerWord ?? null,
|
||||
allowedRoleIds: input.allowedRoleIds,
|
||||
allowedChannelIds: input.allowedChannelIds,
|
||||
createdById
|
||||
}
|
||||
});
|
||||
return toDashboard(tag);
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'P2002') {
|
||||
throw new TagConflictError();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTag(
|
||||
guildId: string,
|
||||
tagId: string,
|
||||
patch: TagDashboardUpdate
|
||||
): Promise<TagDashboard | null> {
|
||||
const existing = await prisma.tag.findFirst({ where: { id: tagId, guildId } });
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await prisma.tag.update({
|
||||
where: { id: tagId },
|
||||
data: {
|
||||
...(patch.name !== undefined ? { name: patch.name } : {}),
|
||||
...(patch.content !== undefined ? { content: patch.content } : {}),
|
||||
...(patch.responseType !== undefined ? { responseType: patch.responseType } : {}),
|
||||
...(patch.triggerWord !== undefined ? { triggerWord: patch.triggerWord } : {}),
|
||||
...(patch.allowedRoleIds !== undefined ? { allowedRoleIds: patch.allowedRoleIds } : {}),
|
||||
...(patch.allowedChannelIds !== undefined ? { allowedChannelIds: patch.allowedChannelIds } : {})
|
||||
}
|
||||
});
|
||||
return toDashboard(updated);
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'P2002') {
|
||||
throw new TagConflictError();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTag(guildId: string, tagId: string): Promise<boolean> {
|
||||
const result = await prisma.tag.deleteMany({ where: { id: tagId, guildId } });
|
||||
return result.count > 0;
|
||||
}
|
||||
Reference in New Issue
Block a user