feat(config): add API route for app configuration and update related logic for monitor guild
This commit is contained in:
+2
-1
@@ -214,7 +214,8 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
||||
const parsed = configSchema.parse(env);
|
||||
return {
|
||||
...parsed,
|
||||
EFFECTIVE_TEXT_GUILD_ID: parsed.TEXT_GUILD_ID ?? parsed.MONITOR_GUILD_ID,
|
||||
// AI text capture and analytics are pinned to the monitor guild.
|
||||
EFFECTIVE_TEXT_GUILD_ID: parsed.MONITOR_GUILD_ID,
|
||||
EFFECTIVE_VOICE_GUILD_ID: parsed.VOICE_GUILD_ID ?? parsed.GUILD_ID,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { createChildLogger } from "../logger.js";
|
||||
import type { MediaController } from "../media/mediaController.js";
|
||||
import type { ModerationBroadcaster } from "../moderation/types.js";
|
||||
import { createAnalysisRoutes } from "../routes/analysisRoutes.js";
|
||||
import { createAppConfigRoutes } from "../routes/appConfigRoutes.js";
|
||||
import { createAnalyticsRoutes } from "../routes/analyticsRoutes.js";
|
||||
import { createMediaRoutes } from "../routes/mediaRoutes.js";
|
||||
import { createMessageRoutes } from "../routes/messageRoutes.js";
|
||||
@@ -115,6 +116,7 @@ export function createHttpApp(options: CreateHttpAppOptions) {
|
||||
);
|
||||
app.use("/api", createMessageRoutes());
|
||||
app.use("/api", createAnalysisRoutes());
|
||||
app.use("/api", createAppConfigRoutes());
|
||||
app.use("/api", createReviewRoutes());
|
||||
app.use("/api", createAnalyticsRoutes());
|
||||
app.use("/api", createSyncRoutes(options.client));
|
||||
|
||||
@@ -235,18 +235,25 @@ export async function getMessagesByChannel(
|
||||
channelId: string,
|
||||
limit: number = 50,
|
||||
offset: number = 0,
|
||||
guildId?: string,
|
||||
): Promise<MessageRecord[]> {
|
||||
try {
|
||||
const database = db();
|
||||
const conditions: SQL[] = [
|
||||
or(
|
||||
eq(messagesTable.channel_id, channelId),
|
||||
eq(messagesTable.thread_id, channelId),
|
||||
) as SQL,
|
||||
];
|
||||
|
||||
if (guildId) {
|
||||
conditions.push(eq(messagesTable.guild_id, guildId));
|
||||
}
|
||||
|
||||
const rows = await database
|
||||
.select()
|
||||
.from(messagesTable)
|
||||
.where(
|
||||
or(
|
||||
eq(messagesTable.channel_id, channelId),
|
||||
eq(messagesTable.thread_id, channelId),
|
||||
),
|
||||
)
|
||||
.where(and(...conditions))
|
||||
// P3: add secondary sort by id for stable pagination
|
||||
.orderBy(desc(messagesTable.created_at), desc(messagesTable.id))
|
||||
.limit(limit)
|
||||
@@ -290,18 +297,25 @@ export async function getAttachmentsByChannel(
|
||||
channelId: string,
|
||||
limit: number = 50,
|
||||
offset: number = 0,
|
||||
guildId?: string,
|
||||
): Promise<AttachmentRecord[]> {
|
||||
try {
|
||||
const database = db();
|
||||
const conditions: SQL[] = [
|
||||
or(
|
||||
eq(attachmentsTable.channel_id, channelId),
|
||||
eq(attachmentsTable.thread_id, channelId),
|
||||
) as SQL,
|
||||
];
|
||||
|
||||
if (guildId) {
|
||||
conditions.push(eq(attachmentsTable.guild_id, guildId));
|
||||
}
|
||||
|
||||
const rows = await database
|
||||
.select()
|
||||
.from(attachmentsTable)
|
||||
.where(
|
||||
or(
|
||||
eq(attachmentsTable.channel_id, channelId),
|
||||
eq(attachmentsTable.thread_id, channelId),
|
||||
),
|
||||
)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(attachmentsTable.created_at))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
@@ -732,15 +746,20 @@ export async function getAttachmentsForMessages(
|
||||
export async function searchMessages(input: {
|
||||
query: string;
|
||||
channelId?: string;
|
||||
guildId?: string;
|
||||
limit?: number;
|
||||
}): Promise<MessageRecord[]> {
|
||||
try {
|
||||
const { query, channelId, limit = 20 } = input;
|
||||
const { query, channelId, guildId, limit = 20 } = input;
|
||||
const database = db();
|
||||
|
||||
const searchPattern = `%${query}%`;
|
||||
const conditions: (SQL | undefined)[] = [isNull(messagesTable.deleted_at)];
|
||||
|
||||
if (guildId) {
|
||||
conditions.push(eq(messagesTable.guild_id, guildId));
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
conditions.push(channelOrThreadCondition(channelId));
|
||||
}
|
||||
@@ -767,6 +786,7 @@ export async function searchMessages(input: {
|
||||
{
|
||||
query: input.query,
|
||||
channelId: input.channelId,
|
||||
guildId: input.guildId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
"Failed to search messages",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Router } from "express";
|
||||
import express from "express";
|
||||
import { config } from "../config.js";
|
||||
import { AppError } from "../errors.js";
|
||||
import {
|
||||
getAnalysisQueueStatus,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
} from "../moderation/aiAnalyzer.js";
|
||||
import {
|
||||
searchMessages,
|
||||
getMessageById,
|
||||
updateMessageAIAnalysis,
|
||||
} from "../moderation/messageStore.js";
|
||||
import type { MessageRecord } from "../moderation/types.js";
|
||||
@@ -49,6 +51,7 @@ export function createAnalysisRoutes(): Router {
|
||||
|
||||
const results = await searchMessages({
|
||||
query: q,
|
||||
guildId: config.MONITOR_GUILD_ID,
|
||||
channelId,
|
||||
limit: limitNum,
|
||||
});
|
||||
@@ -78,6 +81,19 @@ export function createAnalysisRoutes(): Router {
|
||||
throw new AppError("Message ID is required", "MISSING_MESSAGE_ID", 400);
|
||||
}
|
||||
|
||||
const existing = await getMessageById(id);
|
||||
if (!existing) {
|
||||
throw new AppError("Message not found", "MESSAGE_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
if (existing.guild_id !== config.MONITOR_GUILD_ID) {
|
||||
throw new AppError(
|
||||
"Message is outside the monitor guild",
|
||||
"INVALID_GUILD",
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
// P3: Single UPDATE + RETURNING instead of GET + UPDATE + GET
|
||||
const updated = await updateMessageAIAnalysis(id, {
|
||||
status: "pending",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Router } from "express";
|
||||
import express from "express";
|
||||
import { config } from "../config.js";
|
||||
import { AppError } from "../errors.js";
|
||||
import {
|
||||
getActivityHeatmap,
|
||||
@@ -15,6 +16,26 @@ import {
|
||||
export function createAnalyticsRoutes(): Router {
|
||||
const router = express.Router();
|
||||
|
||||
function assertMonitorGuild(guildId?: string): string {
|
||||
if (!config.MONITOR_GUILD_ID) {
|
||||
throw new AppError(
|
||||
"MONITOR_GUILD_ID is required for analytics",
|
||||
"MISSING_MONITOR_GUILD_ID",
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
if (guildId && guildId !== config.MONITOR_GUILD_ID) {
|
||||
throw new AppError(
|
||||
"Analytics are restricted to the monitor guild",
|
||||
"INVALID_GUILD",
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
return config.MONITOR_GUILD_ID;
|
||||
}
|
||||
|
||||
// GET /api/analytics/overview - Full analytics dashboard data
|
||||
// Query params: guildId (required), channelId, hours (default 24)
|
||||
router.get("/analytics/overview", async (req, res, next) => {
|
||||
@@ -34,9 +55,10 @@ export function createAnalyticsRoutes(): Router {
|
||||
}
|
||||
|
||||
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
|
||||
const monitorGuildId = assertMonitorGuild(guildId);
|
||||
|
||||
const overview = await getAnalyticsOverview({
|
||||
guildId,
|
||||
guildId: monitorGuildId,
|
||||
channelId,
|
||||
hours: hoursNum,
|
||||
});
|
||||
@@ -66,9 +88,10 @@ export function createAnalyticsRoutes(): Router {
|
||||
}
|
||||
|
||||
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
|
||||
const monitorGuildId = assertMonitorGuild(guildId);
|
||||
|
||||
const stats = await getHourlyStats({
|
||||
guildId,
|
||||
guildId: monitorGuildId,
|
||||
channelId,
|
||||
hours: hoursNum,
|
||||
});
|
||||
@@ -98,9 +121,10 @@ export function createAnalyticsRoutes(): Router {
|
||||
}
|
||||
|
||||
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
|
||||
const monitorGuildId = assertMonitorGuild(guildId);
|
||||
|
||||
const topics = await getTopicTrends({
|
||||
guildId,
|
||||
guildId: monitorGuildId,
|
||||
channelId,
|
||||
hours: hoursNum,
|
||||
});
|
||||
@@ -132,9 +156,10 @@ export function createAnalyticsRoutes(): Router {
|
||||
|
||||
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
|
||||
const limitNum = limit ? Math.min(parseInt(limit) || 20, 100) : 20;
|
||||
const monitorGuildId = assertMonitorGuild(guildId);
|
||||
|
||||
const users = await getUserLeaderboard({
|
||||
guildId,
|
||||
guildId: monitorGuildId,
|
||||
channelId,
|
||||
hours: hoursNum,
|
||||
limit: limitNum,
|
||||
@@ -165,9 +190,10 @@ export function createAnalyticsRoutes(): Router {
|
||||
}
|
||||
|
||||
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
|
||||
const monitorGuildId = assertMonitorGuild(guildId);
|
||||
|
||||
const stats = await getModerationStats({
|
||||
guildId,
|
||||
guildId: monitorGuildId,
|
||||
channelId,
|
||||
hours: hoursNum,
|
||||
});
|
||||
@@ -199,9 +225,10 @@ export function createAnalyticsRoutes(): Router {
|
||||
|
||||
const hoursNum = hours ? Math.min(parseInt(hours) || 24, 168) : 24;
|
||||
const limitNum = limit ? Math.min(parseInt(limit) || 20, 100) : 20;
|
||||
const monitorGuildId = assertMonitorGuild(guildId);
|
||||
|
||||
const violators = await getTopViolators({
|
||||
guildId,
|
||||
guildId: monitorGuildId,
|
||||
channelId,
|
||||
hours: hoursNum,
|
||||
limit: limitNum,
|
||||
@@ -232,9 +259,10 @@ export function createAnalyticsRoutes(): Router {
|
||||
}
|
||||
|
||||
const hoursNum = hours ? Math.min(parseInt(hours) || 168, 720) : 168;
|
||||
const monitorGuildId = assertMonitorGuild(guildId);
|
||||
|
||||
const trend = await getDailyTrend({
|
||||
guildId,
|
||||
guildId: monitorGuildId,
|
||||
channelId,
|
||||
hours: hoursNum,
|
||||
});
|
||||
@@ -264,9 +292,10 @@ export function createAnalyticsRoutes(): Router {
|
||||
}
|
||||
|
||||
const hoursNum = hours ? Math.min(parseInt(hours) || 168, 720) : 168;
|
||||
const monitorGuildId = assertMonitorGuild(guildId);
|
||||
|
||||
const heatmap = await getActivityHeatmap({
|
||||
guildId,
|
||||
guildId: monitorGuildId,
|
||||
channelId,
|
||||
hours: hoursNum,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Router } from "express";
|
||||
import express from "express";
|
||||
import { config } from "../config.js";
|
||||
|
||||
export function createAppConfigRoutes(): Router {
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/config", (_req, res) => {
|
||||
res.json({
|
||||
monitorGuildId: config.MONITOR_GUILD_ID ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Router } from "express";
|
||||
import express from "express";
|
||||
import { config } from "../config.js";
|
||||
import { AppError } from "../errors.js";
|
||||
import {
|
||||
getAttachmentsByChannel,
|
||||
@@ -52,6 +53,7 @@ export function createMessageRoutes(): Router {
|
||||
};
|
||||
|
||||
const targetChannel = channelId || channel;
|
||||
const monitorGuildId = config.MONITOR_GUILD_ID;
|
||||
const limitNum = Math.min(parseInt(limit) || 50, 100);
|
||||
const offsetNum = parseInt(offset) || 0;
|
||||
|
||||
@@ -68,6 +70,7 @@ export function createMessageRoutes(): Router {
|
||||
targetChannel,
|
||||
limitNum,
|
||||
offsetNum,
|
||||
monitorGuildId,
|
||||
);
|
||||
res.json({
|
||||
type: "image",
|
||||
@@ -77,6 +80,7 @@ export function createMessageRoutes(): Router {
|
||||
});
|
||||
} else if (channelId || cursor || status) {
|
||||
const result = await listMessages({
|
||||
guildId: monitorGuildId,
|
||||
channelId: targetChannel,
|
||||
cursor,
|
||||
limit: limitNum,
|
||||
@@ -101,6 +105,7 @@ export function createMessageRoutes(): Router {
|
||||
targetChannel,
|
||||
limitNum,
|
||||
offsetNum,
|
||||
monitorGuildId,
|
||||
);
|
||||
res.json({
|
||||
type: "text",
|
||||
@@ -129,6 +134,14 @@ export function createMessageRoutes(): Router {
|
||||
throw new AppError("Message not found", "MESSAGE_NOT_FOUND", 404);
|
||||
}
|
||||
|
||||
if (message.guild_id !== config.MONITOR_GUILD_ID) {
|
||||
throw new AppError(
|
||||
"Message is outside the monitor guild",
|
||||
"INVALID_GUILD",
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
res.json(message);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@@ -158,7 +171,7 @@ export function createMessageRoutes(): Router {
|
||||
const limitNum = Math.min(parseInt(limit) || 50, 100);
|
||||
|
||||
const query: Omit<MessageQuery, "status"> = {
|
||||
guildId,
|
||||
guildId: guildId || config.MONITOR_GUILD_ID,
|
||||
channelId,
|
||||
threadId,
|
||||
userId,
|
||||
|
||||
Reference in New Issue
Block a user