Add suggestion status update job and queue integration
- Introduced a new `SuggestionStatusUpdateJob` type and corresponding worker to handle suggestion status updates in the bot's job processing. - Added `suggestionsQueue` and `suggestionsQueueEvents` for managing suggestion-related jobs in both the bot and WebUI. - Implemented `runSuggestionStatusUpdateJob` to facilitate staff actions on suggestions via the WebUI, ensuring consistent behavior with existing commands. - Enhanced error handling for suggestion actions to improve user experience and reliability.
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import type { SocialFeedDashboard, SocialFeedDashboardCreate, SocialFeedDashboardUpdate } from '@nexumi/shared';
|
||||
import type {
|
||||
SocialFeedDashboard,
|
||||
SocialFeedDashboardCreate,
|
||||
SocialFeedDashboardUpdate
|
||||
} from '@nexumi/shared';
|
||||
import type { SocialFeed } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
|
||||
@@ -14,12 +18,15 @@ function toDashboard(feed: SocialFeed): SocialFeedDashboard {
|
||||
};
|
||||
}
|
||||
|
||||
export async function listSocialFeeds(guildId: string): Promise<SocialFeedDashboard[]> {
|
||||
const feeds = await prisma.socialFeed.findMany({ where: { guildId }, orderBy: { createdAt: 'asc' } });
|
||||
export async function listFeeds(guildId: string): Promise<SocialFeedDashboard[]> {
|
||||
const feeds = await prisma.socialFeed.findMany({
|
||||
where: { guildId },
|
||||
orderBy: { createdAt: 'asc' }
|
||||
});
|
||||
return feeds.map(toDashboard);
|
||||
}
|
||||
|
||||
export async function createSocialFeed(
|
||||
export async function createFeed(
|
||||
guildId: string,
|
||||
input: SocialFeedDashboardCreate
|
||||
): Promise<SocialFeedDashboard> {
|
||||
@@ -30,7 +37,7 @@ export async function createSocialFeed(
|
||||
type: input.type,
|
||||
sourceId: input.sourceId,
|
||||
channelId: input.channelId,
|
||||
rolePingId: input.rolePingId ? input.rolePingId : null,
|
||||
rolePingId: input.rolePingId || null,
|
||||
template: input.template ?? null,
|
||||
enabled: input.enabled
|
||||
}
|
||||
@@ -38,7 +45,7 @@ export async function createSocialFeed(
|
||||
return toDashboard(feed);
|
||||
}
|
||||
|
||||
export async function updateSocialFeed(
|
||||
export async function updateFeed(
|
||||
guildId: string,
|
||||
feedId: string,
|
||||
patch: SocialFeedDashboardUpdate
|
||||
@@ -59,7 +66,11 @@ export async function updateSocialFeed(
|
||||
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;
|
||||
export async function deleteFeed(guildId: string, feedId: string): Promise<boolean> {
|
||||
const existing = await prisma.socialFeed.findFirst({ where: { id: feedId, guildId } });
|
||||
if (!existing) {
|
||||
return false;
|
||||
}
|
||||
await prisma.socialFeed.delete({ where: { id: feedId } });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,13 @@ import type {
|
||||
} from '@nexumi/shared';
|
||||
import type { Suggestion } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import { addJobAndAwait, suggestionsQueue, suggestionsQueueEvents } from '../queues';
|
||||
|
||||
export class SuggestionActionTimeoutError extends Error {
|
||||
constructor() {
|
||||
super('The bot did not confirm the suggestion update in time. Reload the list shortly to check the result.');
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureSuggestionConfig(guildId: string) {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
@@ -53,7 +60,7 @@ export async function updateSuggestionConfigDashboard(
|
||||
return toConfigDashboard(updated);
|
||||
}
|
||||
|
||||
function toSuggestionDashboard(suggestion: Suggestion): SuggestionDashboard {
|
||||
function toDashboard(suggestion: Suggestion): SuggestionDashboard {
|
||||
return {
|
||||
id: suggestion.id,
|
||||
authorId: suggestion.authorId,
|
||||
@@ -75,25 +82,48 @@ export async function listSuggestions(
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: query.limit
|
||||
});
|
||||
return suggestions.map(toSuggestionDashboard);
|
||||
return suggestions.map(toDashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Staff actions (approve/deny/consider/implement) need to move or edit the
|
||||
* suggestion's Discord embed and DM the author, which requires the bot's
|
||||
* discord.js Client. The WebUI enqueues a `suggestionStatusUpdate` job on
|
||||
* the same `suggestions` BullMQ queue the bot worker consumes (see
|
||||
* `apps/bot/src/modules/suggestions/service.ts#runSuggestionStatusUpdateJob`)
|
||||
* and waits (bounded) for it to finish.
|
||||
*/
|
||||
export async function applySuggestionAction(
|
||||
guildId: string,
|
||||
suggestionId: string,
|
||||
action: SuggestionAction
|
||||
): Promise<SuggestionDashboard | null> {
|
||||
action: SuggestionAction,
|
||||
_actorUserId: string
|
||||
): Promise<SuggestionDashboard> {
|
||||
const existing = await prisma.suggestion.findFirst({ where: { id: suggestionId, guildId } });
|
||||
if (!existing) {
|
||||
return null;
|
||||
throw new SuggestionActionTimeoutError();
|
||||
}
|
||||
|
||||
const updated = await prisma.suggestion.update({
|
||||
where: { id: suggestionId },
|
||||
data: {
|
||||
const { confirmed } = await addJobAndAwait<{ id: string }>(
|
||||
suggestionsQueue,
|
||||
suggestionsQueueEvents,
|
||||
'suggestionStatusUpdate',
|
||||
{
|
||||
suggestionId,
|
||||
guildId,
|
||||
status: action.status,
|
||||
staffReason: action.staffReason ?? null
|
||||
}
|
||||
});
|
||||
return toSuggestionDashboard(updated);
|
||||
reason: action.staffReason ?? ''
|
||||
},
|
||||
{ removeOnComplete: true, removeOnFail: 25 }
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
throw new SuggestionActionTimeoutError();
|
||||
}
|
||||
|
||||
const updated = await prisma.suggestion.findUnique({ where: { id: suggestionId } });
|
||||
if (!updated) {
|
||||
throw new SuggestionActionTimeoutError();
|
||||
}
|
||||
return toDashboard(updated);
|
||||
}
|
||||
|
||||
@@ -12,13 +12,16 @@ import { redis } from './redis';
|
||||
export const giveawayQueueName = 'giveaways';
|
||||
export const scheduleQueueName = 'schedules';
|
||||
export const guildBackupQueueName = 'guild-backups';
|
||||
export const suggestionsQueueName = 'suggestions';
|
||||
|
||||
const globalForQueues = globalThis as unknown as {
|
||||
__nexumiGiveawayQueue?: Queue;
|
||||
__nexumiScheduleQueue?: Queue;
|
||||
__nexumiGuildBackupQueue?: Queue;
|
||||
__nexumiSuggestionsQueue?: Queue;
|
||||
__nexumiGiveawayQueueEvents?: QueueEvents;
|
||||
__nexumiGuildBackupQueueEvents?: QueueEvents;
|
||||
__nexumiSuggestionsQueueEvents?: QueueEvents;
|
||||
};
|
||||
|
||||
export const giveawayQueue: Queue =
|
||||
@@ -30,6 +33,9 @@ export const scheduleQueue: Queue =
|
||||
export const guildBackupQueue: Queue =
|
||||
globalForQueues.__nexumiGuildBackupQueue ?? new Queue(guildBackupQueueName, { connection: redis });
|
||||
|
||||
export const suggestionsQueue: Queue =
|
||||
globalForQueues.__nexumiSuggestionsQueue ?? new Queue(suggestionsQueueName, { connection: redis });
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -44,11 +50,17 @@ export const guildBackupQueueEvents: QueueEvents =
|
||||
globalForQueues.__nexumiGuildBackupQueueEvents ??
|
||||
new QueueEvents(guildBackupQueueName, { connection: redis.duplicate() });
|
||||
|
||||
export const suggestionsQueueEvents: QueueEvents =
|
||||
globalForQueues.__nexumiSuggestionsQueueEvents ??
|
||||
new QueueEvents(suggestionsQueueName, { connection: redis.duplicate() });
|
||||
|
||||
globalForQueues.__nexumiGiveawayQueue = giveawayQueue;
|
||||
globalForQueues.__nexumiScheduleQueue = scheduleQueue;
|
||||
globalForQueues.__nexumiGuildBackupQueue = guildBackupQueue;
|
||||
globalForQueues.__nexumiSuggestionsQueue = suggestionsQueue;
|
||||
globalForQueues.__nexumiGiveawayQueueEvents = giveawayQueueEvents;
|
||||
globalForQueues.__nexumiGuildBackupQueueEvents = guildBackupQueueEvents;
|
||||
globalForQueues.__nexumiSuggestionsQueueEvents = suggestionsQueueEvents;
|
||||
|
||||
const DEFAULT_WAIT_TIMEOUT_MS = 15_000;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user