Refactor code for improved readability and consistency

- Adjusted formatting in conversationContext.ts for better token estimation readability.
- Enhanced readability in indonesianTextNormalizer.ts by formatting multiline replacements.
- Reformatted badword lists and whitelists in indonesianTextNormalizer.ts for consistency.
- Improved function signatures in messageStore.ts for clarity.
- Reformatted messageCapture.ts to enhance readability of channel ID checks.
- Cleaned up error logging in messageStore.ts and retentionManager.ts for better clarity.
- Reformatted indonesianSlangLexicon.ts for consistent object formatting.
- Enhanced URL fetching regex patterns in urlFetcher.ts for better readability.
- Simplified query parameter destructuring in analyticsRoutes.ts for cleaner code.
- Improved test readability in autoDeleteManager.test.ts and indonesianTextNormalizer.test.ts by formatting expectations.
- Cleaned up whitespace in messageCaptureFilter.test.ts for consistency.
This commit is contained in:
MythEclipse
2026-05-30 15:55:01 +07:00
parent 4fa7875b80
commit 551d11ba82
18 changed files with 697 additions and 224 deletions
+3 -1
View File
@@ -15,7 +15,9 @@ export async function initializeApp() {
const logger = createChildLogger("bot");
if (!config.AI_LLM_API_KEY) {
logger.error("AI_LLM_API_KEY is missing from environment. Force closing application as AI environment is required.");
logger.error(
"AI_LLM_API_KEY is missing from environment. Force closing application as AI environment is required.",
);
process.exit(1);
}
+22 -8
View File
@@ -493,9 +493,13 @@ export const pgMessageReviewsTable = pgTable(
reviewed_at: pgBigint("reviewed_at", { mode: "number" }),
},
(table) => ({
messageIdIdx: pgIndex("idx_message_reviews_message_id").on(table.message_id),
messageIdIdx: pgIndex("idx_message_reviews_message_id").on(
table.message_id,
),
statusIdx: pgIndex("idx_message_reviews_status").on(table.status),
createdAtIdx: pgIndex("idx_message_reviews_created_at").on(table.created_at),
createdAtIdx: pgIndex("idx_message_reviews_created_at").on(
table.created_at,
),
guildStatusIdx: pgIndex("idx_message_reviews_guild_status").on(
table.guild_id,
table.status,
@@ -553,9 +557,14 @@ export const pgModerationActionsTable = pgTable(
user_id: pgText("user_id"),
guild_id: pgText("guild_id").notNull(),
action_type: pgText("action_type", {
enum: ["delete_message", "mute_user", "warn_user", "kick_user", "ban_user"],
})
.notNull(),
enum: [
"delete_message",
"mute_user",
"warn_user",
"kick_user",
"ban_user",
],
}).notNull(),
reason: pgText("reason"),
executed_by: pgText("executed_by"),
status: pgText("status", {
@@ -593,9 +602,14 @@ export const sqliteModerationActionsTable = sqliteTable(
user_id: sqliteText("user_id"),
guild_id: sqliteText("guild_id").notNull(),
action_type: sqliteText("action_type", {
enum: ["delete_message", "mute_user", "warn_user", "kick_user", "ban_user"],
})
.notNull(),
enum: [
"delete_message",
"mute_user",
"warn_user",
"kick_user",
"ban_user",
],
}).notNull(),
reason: sqliteText("reason"),
executed_by: sqliteText("executed_by"),
status: sqliteText("status", {
+3 -11
View File
@@ -1,9 +1,6 @@
import type { Client, Guild, User } from "discord.js-selfbot-v13";
import { createChildLogger } from "../logger.js";
import {
getModerationAction,
updateModerationAction,
} from "./messageStore.js";
import { getModerationAction, updateModerationAction } from "./messageStore.js";
import type { ModerationAction, ModerationActionType } from "./types.js";
const logger = createChildLogger("action-executor");
@@ -151,9 +148,7 @@ async function executeWarnUser(
}
const reason = action.reason || "Warned by moderation system";
await user.send(
`You have been warned in ${guild.name}. Reason: ${reason}`,
);
await user.send(`You have been warned in ${guild.name}. Reason: ${reason}`);
logger.info(
{ userId: action.user_id, guildId: guild.id },
@@ -257,10 +252,7 @@ export async function processPendingActions(
}
}
logger.info(
{ guildId, ...result },
"Processed pending moderation actions",
);
logger.info({ guildId, ...result }, "Processed pending moderation actions");
return result;
} catch (error) {
+9 -6
View File
@@ -45,12 +45,15 @@ export default async function processAnalysisRequest({
messages,
}: AnalysisWorkerRequest): Promise<AnalysisWorkerResponse> {
if (!config.AI_LLM_API_KEY) {
console.error(JSON.stringify({
level: "FATAL",
context: "aiAnalysisWorker",
error: "AI_LLM_API_KEY is missing from environment. Force closing worker operation.",
timestamp: new Date().toISOString(),
}));
console.error(
JSON.stringify({
level: "FATAL",
context: "aiAnalysisWorker",
error:
"AI_LLM_API_KEY is missing from environment. Force closing worker operation.",
timestamp: new Date().toISOString(),
}),
);
process.exit(1);
}
+1 -3
View File
@@ -7,9 +7,7 @@ import { config } from "../config.js";
import { createChildLogger } from "../logger.js";
import { retryWithBackoff } from "../retry.js";
import { attemptAutoDeleteFlaggedMessage } from "./autoDeleteManager.js";
import {
buildConversationContext,
} from "./conversationContext.js";
import { buildConversationContext } from "./conversationContext.js";
import { runModerationAnalysis } from "./llmModerationClient.js";
import {
getAttachmentsForMessages,
+226 -49
View File
@@ -110,7 +110,13 @@ export async function getHourlyStats(input: {
// Initialize all hour buckets
const buckets = new Map<
string,
{ count: number; clean: number; warned: number; flagged: number; error: number }
{
count: number;
clean: number;
warned: number;
flagged: number;
error: number;
}
>();
for (let h = 0; h < hours; h++) {
@@ -151,23 +157,156 @@ export async function getHourlyStats(input: {
// ── Topic Trends ───────────────────────────────────────────────────────
const STOP_WORDS = new Set([
"yang", "dan", "itu", "ini", "dengan", "akan", "pada", "dari", "di", "ke",
"untuk", "tidak", "ada", "juga", "sudah", "saya", "kamu", "dia", "mereka",
"kami", "aku", "lo", "lu", "gua", "gue", "org", "orang", "aja", "sama",
"kalo", "kalau", "bisa", "karena", "gak", "nggak", "ga", "tak", "belum",
"udah", "dah", "lah", "kah", "pun", "nih", "tuh", "deh", "dong", "si",
"nya", "kan", "ya", "yah", "yuk", "kok", "loh", "nah", "wow", "eh",
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
"have", "has", "had", "having", "do", "does", "did", "doing",
"will", "would", "could", "should", "may", "might", "must", "shall",
"i", "you", "he", "she", "it", "we", "they", "me", "him", "her",
"us", "them", "my", "your", "his", "its", "our", "their",
"and", "but", "or", "nor", "not", "so", "yet", "for", "if",
"to", "of", "in", "on", "at", "by", "as", "with", "about",
"just", "then", "now", "here", "there", "when", "where", "why",
"how", "all", "both", "each", "few", "more", "most", "other",
"some", "such", "only", "own", "same", "too", "very", "can",
"go", "ok", "okay", "yeah", "yes", "no",
"yang",
"dan",
"itu",
"ini",
"dengan",
"akan",
"pada",
"dari",
"di",
"ke",
"untuk",
"tidak",
"ada",
"juga",
"sudah",
"saya",
"kamu",
"dia",
"mereka",
"kami",
"aku",
"lo",
"lu",
"gua",
"gue",
"org",
"orang",
"aja",
"sama",
"kalo",
"kalau",
"bisa",
"karena",
"gak",
"nggak",
"ga",
"tak",
"belum",
"udah",
"dah",
"lah",
"kah",
"pun",
"nih",
"tuh",
"deh",
"dong",
"si",
"nya",
"kan",
"ya",
"yah",
"yuk",
"kok",
"loh",
"nah",
"wow",
"eh",
"the",
"a",
"an",
"is",
"are",
"was",
"were",
"be",
"been",
"being",
"have",
"has",
"had",
"having",
"do",
"does",
"did",
"doing",
"will",
"would",
"could",
"should",
"may",
"might",
"must",
"shall",
"i",
"you",
"he",
"she",
"it",
"we",
"they",
"me",
"him",
"her",
"us",
"them",
"my",
"your",
"his",
"its",
"our",
"their",
"and",
"but",
"or",
"nor",
"not",
"so",
"yet",
"for",
"if",
"to",
"of",
"in",
"on",
"at",
"by",
"as",
"with",
"about",
"just",
"then",
"now",
"here",
"there",
"when",
"where",
"why",
"how",
"all",
"both",
"each",
"few",
"more",
"most",
"other",
"some",
"such",
"only",
"own",
"same",
"too",
"very",
"can",
"go",
"ok",
"okay",
"yeah",
"yes",
"no",
]);
function extractTopics(messages: MessageRecord[], topN = 15): TopicTrend[] {
@@ -182,22 +321,36 @@ function extractTopics(messages: MessageRecord[], topN = 15): TopicTrend[] {
const topics = analysis.topics;
if (topics && Array.isArray(topics)) {
for (const topic of topics) {
const key = typeof topic === "string" ? topic : topic.name || topic.topic;
const key =
typeof topic === "string" ? topic : topic.name || topic.topic;
if (!key) continue;
const k = key.toLowerCase();
const score = msg.ai_moderation_score || 0;
const existing = topicScores.get(k);
if (existing) { existing.count++; existing.score += score; }
else { topicScores.set(k, { count: 1, score }); }
if (existing) {
existing.count++;
existing.score += score;
} else {
topicScores.set(k, { count: 1, score });
}
}
}
if (analysis.category) {
const cat = String(analysis.category).toLowerCase();
const existing = topicScores.get(cat);
if (existing) { existing.count++; existing.score += msg.ai_moderation_score || 0; }
else { topicScores.set(cat, { count: 1, score: msg.ai_moderation_score || 0 }); }
if (existing) {
existing.count++;
existing.score += msg.ai_moderation_score || 0;
} else {
topicScores.set(cat, {
count: 1,
score: msg.ai_moderation_score || 0,
});
}
}
} catch { /* not valid JSON */ }
} catch {
/* not valid JSON */
}
}
if (msg.content) {
@@ -227,7 +380,11 @@ function extractTopics(messages: MessageRecord[], topN = 15): TopicTrend[] {
for (const [word, count] of sortedWords) {
if (!topicScores.has(word)) {
results.push({ topic: word, count, score: flaggedWordFreq.get(word) || 0 });
results.push({
topic: word,
count,
score: flaggedWordFreq.get(word) || 0,
});
}
}
@@ -308,7 +465,8 @@ export async function getUserLeaderboard(input: {
existing.message_count++;
if (msg.type === "edited") existing.edited_count++;
if (msg.type === "deleted") existing.deleted_count++;
if (msg.ai_status === "flagged" || msg.ai_status === "warn") existing.flagged_count++;
if (msg.ai_status === "flagged" || msg.ai_status === "warn")
existing.flagged_count++;
if (msg.created_at > existing.last_active) {
existing.last_active = msg.created_at;
}
@@ -320,7 +478,8 @@ export async function getUserLeaderboard(input: {
message_count: 1,
edited_count: msg.type === "edited" ? 1 : 0,
deleted_count: msg.type === "deleted" ? 1 : 0,
flagged_count: msg.ai_status === "flagged" || msg.ai_status === "warn" ? 1 : 0,
flagged_count:
msg.ai_status === "flagged" || msg.ai_status === "warn" ? 1 : 0,
last_active: msg.created_at,
});
}
@@ -367,7 +526,11 @@ export async function getModerationStats(input: {
const breakdown: ModerationBreakdown = {
total: rows.length,
clean: 0, warned: 0, flagged: 0, error: 0, pending: 0,
clean: 0,
warned: 0,
flagged: 0,
error: 0,
pending: 0,
average_score: 0,
};
@@ -398,7 +561,13 @@ export async function getModerationStats(input: {
"Failed to get moderation stats",
);
return {
total: 0, clean: 0, warned: 0, flagged: 0, error: 0, pending: 0, average_score: 0,
total: 0,
clean: 0,
warned: 0,
flagged: 0,
error: 0,
pending: 0,
average_score: 0,
};
}
}
@@ -446,7 +615,7 @@ export interface ViolatorStat {
flagged_count: number;
warned_count: number;
violation_score: number; // weighted: flagged*3 + warned*1
worst_flags: string[]; // unique flag types
worst_flags: string[]; // unique flag types
last_violation: number;
}
@@ -477,16 +646,19 @@ export async function getTopViolators(input: {
.where(and(...conditions) as SQL)
.orderBy(asc(messagesTable.created_at))) as MessageRecord[];
const userMap = new Map<string, {
user_id: string;
username: string;
avatar_url: string | null;
total_messages: number;
flagged_count: number;
warned_count: number;
flags_set: Set<string>;
last_violation: number;
}>();
const userMap = new Map<
string,
{
user_id: string;
username: string;
avatar_url: string | null;
total_messages: number;
flagged_count: number;
warned_count: number;
flags_set: Set<string>;
last_violation: number;
}
>();
for (const msg of rows) {
let entry = userMap.get(msg.user_id);
@@ -506,7 +678,8 @@ export async function getTopViolators(input: {
entry.total_messages++;
const isViolation = msg.ai_status === "flagged" || msg.ai_status === "warn";
const isViolation =
msg.ai_status === "flagged" || msg.ai_status === "warn";
if (msg.ai_status === "flagged") {
entry.flagged_count++;
@@ -522,7 +695,9 @@ export async function getTopViolators(input: {
if (Array.isArray(flags)) {
for (const f of flags) entry.flags_set.add(String(f));
}
} catch { /* ignore */ }
} catch {
/* ignore */
}
}
if (isViolation && msg.created_at > entry.last_violation) {
@@ -571,13 +746,15 @@ export async function getAnalyticsOverview(input: {
const now = Date.now();
const since = now - hours * 3600_000;
const [messages, hourly, topics, topUsers, totalChannels] = await Promise.all([
getModerationStats(input),
getHourlyStats(input),
getTopicTrends(input),
getUserLeaderboard(input),
getActiveChannelCount({ guildId, hours }),
]);
const [messages, hourly, topics, topUsers, totalChannels] = await Promise.all(
[
getModerationStats(input),
getHourlyStats(input),
getTopicTrends(input),
getUserLeaderboard(input),
getActiveChannelCount({ guildId, hours }),
],
);
return {
period: { start: since, end: now },
+104 -26
View File
@@ -10,7 +10,9 @@ const parseStringList = (value?: string | null): string[] => {
if (!value) return [];
try {
const parsed = JSON.parse(value) as unknown;
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : [];
return Array.isArray(parsed)
? parsed.filter((item): item is string => typeof item === "string")
: [];
} catch {
return value
.split(",")
@@ -23,7 +25,8 @@ const parseStringList = (value?: string | null): string[] => {
function deriveSeverity(msg: MessageRecord): string {
if (msg.ai_severity) return msg.ai_severity;
const score = msg.ai_confidence ?? msg.ai_moderation_score ?? 0;
if (msg.ai_status === "flagged") return score >= 0.9 ? "critical" : score >= 0.7 ? "high" : "medium";
if (msg.ai_status === "flagged")
return score >= 0.9 ? "critical" : score >= 0.7 ? "high" : "medium";
if (msg.ai_status === "warn") return score >= 0.6 ? "medium" : "low";
return "none";
}
@@ -32,19 +35,28 @@ function deriveSeverity(msg: MessageRecord): string {
function deriveRecommendedAction(msg: MessageRecord): string {
if (msg.ai_recommended_action) return msg.ai_recommended_action;
const severity = deriveSeverity(msg);
if (msg.ai_status === "flagged" && (severity === "critical" || severity === "high")) return "delete";
if (
msg.ai_status === "flagged" &&
(severity === "critical" || severity === "high")
)
return "delete";
if (msg.ai_status === "flagged") return "review";
if (msg.ai_status === "warn") return "warn";
return "none";
}
function isAutoDeleteEligible(message: MessageRecord): boolean {
if (message.ai_status !== "flagged" && message.ai_status !== "warn") return false;
if (message.ai_status !== "flagged" && message.ai_status !== "warn")
return false;
const confidence = message.ai_confidence ?? message.ai_moderation_score ?? 0;
if (confidence < config.AUTO_DELETE_MIN_CONFIDENCE) {
logger.info(
{ messageId: message.id, confidence, threshold: config.AUTO_DELETE_MIN_CONFIDENCE },
{
messageId: message.id,
confidence,
threshold: config.AUTO_DELETE_MIN_CONFIDENCE,
},
"Auto-delete skipped: confidence below threshold",
);
return false;
@@ -72,31 +84,49 @@ function isAutoDeleteEligible(message: MessageRecord): boolean {
return false;
}
const allowedCategories = parseStringList(config.AUTO_DELETE_ALLOWED_CATEGORIES);
const allowedCategories = parseStringList(
config.AUTO_DELETE_ALLOWED_CATEGORIES,
);
if (allowedCategories.length > 0) {
const messageCategories = parseStringList(message.ai_categories ?? message.ai_moderation_flags);
const hasAllowedCategory = messageCategories.some((cat) => allowedCategories.includes(cat));
const messageCategories = parseStringList(
message.ai_categories ?? message.ai_moderation_flags,
);
const hasAllowedCategory = messageCategories.some((cat) =>
allowedCategories.includes(cat),
);
if (!hasAllowedCategory) {
logger.info(
{ messageId: message.id, categories: messageCategories, allowed: allowedCategories },
{
messageId: message.id,
categories: messageCategories,
allowed: allowedCategories,
},
"Auto-delete skipped: no allowed categories match",
);
return false;
}
}
const excludedChannels = parseStringList(config.AUTO_DELETE_EXCLUDED_CHANNEL_IDS);
const excludedChannels = parseStringList(
config.AUTO_DELETE_EXCLUDED_CHANNEL_IDS,
);
if (excludedChannels.length > 0) {
const channelId = message.thread_id ?? message.channel_id;
if (excludedChannels.includes(channelId)) {
logger.info({ messageId: message.id, channelId }, "Auto-delete skipped: channel excluded");
logger.info(
{ messageId: message.id, channelId },
"Auto-delete skipped: channel excluded",
);
return false;
}
}
const excludedUsers = parseStringList(config.AUTO_DELETE_EXCLUDED_USER_IDS);
if (excludedUsers.length > 0 && excludedUsers.includes(message.user_id)) {
logger.info({ messageId: message.id, userId: message.user_id }, "Auto-delete skipped: user excluded");
logger.info(
{ messageId: message.id, userId: message.user_id },
"Auto-delete skipped: user excluded",
);
return false;
}
@@ -115,13 +145,21 @@ async function logAutoDeleteAttempt(
action_type: "delete_message",
reason: result.reason,
executed_by: "auto-delete-manager",
status: result.deleted ? "executed" : result.reason === "dry_run" ? "executed" : "failed",
status: result.deleted
? "executed"
: result.reason === "dry_run"
? "executed"
: "failed",
error: result.reason === "error" ? result.reason : null,
executed_at: result.deleted || result.reason === "dry_run" ? Date.now() : null,
executed_at:
result.deleted || result.reason === "dry_run" ? Date.now() : null,
});
} catch (error) {
logger.warn(
{ messageId: message.id, error: error instanceof Error ? error.message : String(error) },
{
messageId: message.id,
error: error instanceof Error ? error.message : String(error),
},
"Failed to persist auto-delete action log",
);
}
@@ -147,7 +185,11 @@ function isAlreadyDeletedError(error: unknown): boolean {
function hasChannelMessagesApi(
channel: unknown,
): channel is { messages: { fetch: (id: string) => Promise<{ delete: () => Promise<unknown> }> } } {
): channel is {
messages: {
fetch: (id: string) => Promise<{ delete: () => Promise<unknown> }>;
};
} {
return Boolean(
channel &&
typeof channel === "object" &&
@@ -160,12 +202,17 @@ function hasChannelMessagesApi(
function hasPermissionApi(
channel: unknown,
): channel is { permissionsFor: (member: unknown) => { has: (permission: string) => boolean } | null } {
): channel is {
permissionsFor: (
member: unknown,
) => { has: (permission: string) => boolean } | null;
} {
return Boolean(
channel &&
typeof channel === "object" &&
"permissionsFor" in channel &&
typeof (channel as { permissionsFor?: unknown }).permissionsFor === "function",
typeof (channel as { permissionsFor?: unknown }).permissionsFor ===
"function",
);
}
@@ -178,19 +225,30 @@ export async function attemptAutoDeleteFlaggedMessage(
}
if (message.ai_status !== "flagged" && message.ai_status !== "warn") {
const result = { deleted: false, skipped: true, reason: "not_flagged_or_warn" } as AutoDeleteResult;
const result = {
deleted: false,
skipped: true,
reason: "not_flagged_or_warn",
} as AutoDeleteResult;
await logAutoDeleteAttempt(message, result);
return result;
}
if (!isAutoDeleteEligible(message)) {
const result = { deleted: false, skipped: true, reason: "not_eligible" } as AutoDeleteResult;
const result = {
deleted: false,
skipped: true,
reason: "not_eligible",
} as AutoDeleteResult;
await logAutoDeleteAttempt(message, result);
return result;
}
if (!client?.user?.id) {
logger.warn({ messageId: message.id }, "Auto-delete skipped: client user missing");
logger.warn(
{ messageId: message.id },
"Auto-delete skipped: client user missing",
);
return { deleted: false, skipped: true, reason: "client_user_missing" };
}
@@ -232,11 +290,19 @@ export async function attemptAutoDeleteFlaggedMessage(
{ messageId: message.id, channelId, userId: client.user.id },
"Auto-delete skipped: current user lacks Manage Messages",
);
return { deleted: false, skipped: true, reason: "missing_manage_messages" };
return {
deleted: false,
skipped: true,
reason: "missing_manage_messages",
};
}
if (config.AUTO_DELETE_FLAGGED_DRY_RUN) {
const result = { deleted: false, skipped: true, reason: "dry_run" } as AutoDeleteResult;
const result = {
deleted: false,
skipped: true,
reason: "dry_run",
} as AutoDeleteResult;
await logAutoDeleteAttempt(message, result);
logger.info(
{ messageId: message.id, channelId },
@@ -248,7 +314,11 @@ export async function attemptAutoDeleteFlaggedMessage(
const discordMessage = await channel.messages.fetch(message.id);
await discordMessage.delete();
const result = { deleted: true, skipped: false, reason: "deleted" } as AutoDeleteResult;
const result = {
deleted: true,
skipped: false,
reason: "deleted",
} as AutoDeleteResult;
await logAutoDeleteAttempt(message, result);
logger.info(
{ messageId: message.id, channelId },
@@ -257,7 +327,11 @@ export async function attemptAutoDeleteFlaggedMessage(
return result;
} catch (error) {
if (isAlreadyDeletedError(error)) {
const result = { deleted: true, skipped: false, reason: "already_deleted" } as AutoDeleteResult;
const result = {
deleted: true,
skipped: false,
reason: "already_deleted",
} as AutoDeleteResult;
await logAutoDeleteAttempt(message, result);
logger.info(
{ messageId: message.id, code: getErrorCode(error) },
@@ -266,7 +340,11 @@ export async function attemptAutoDeleteFlaggedMessage(
return result;
}
const result = { deleted: false, skipped: true, reason: "error" } as AutoDeleteResult;
const result = {
deleted: false,
skipped: true,
reason: "error",
} as AutoDeleteResult;
await logAutoDeleteAttempt(message, result);
logger.error(
{
+4 -1
View File
@@ -51,7 +51,10 @@ export async function buildConversationContext(
const targetLines = await Promise.all(
targets.map((msg) => formatMessageForPrompt(msg, "target")),
);
let usedTokens = targetLines.reduce((sum, line) => sum + estimateTokens(line), 0);
let usedTokens = targetLines.reduce(
(sum, line) => sum + estimateTokens(line),
0,
);
const selectedContextLines: string[] = [];
+60 -15
View File
@@ -53,10 +53,13 @@ export function normalizeDiscordCustomEmoji(text: string): {
emojiNames: string[];
} {
const emojiNames: string[] = [];
const normalized = text.replace(CUSTOM_EMOJI_PATTERN, (_match, name: string) => {
emojiNames.push(name);
return `[emoji:${name}]`;
});
const normalized = text.replace(
CUSTOM_EMOJI_PATTERN,
(_match, name: string) => {
emojiNames.push(name);
return `[emoji:${name}]`;
},
);
return { text: normalized, emojiNames };
}
@@ -82,16 +85,47 @@ export function normalizeIndonesianSlang(text: string): {
// ---------------------------------------------------------------------------
const LOCAL_BADWORDS = [
"anjing", "bangsat", "brengsek", "bajingan", "kontol", "memek",
"tai", "goblok", "tolol", "bego", "sialan", "jancuk", "kampret",
"pepek", "jembut", "ngentot", "ngewe", "coli", "celaka", "laknat",
"pantek", "entod", "ndasmu", "ndas", "piyo", "asu",
"anjing",
"bangsat",
"brengsek",
"bajingan",
"kontol",
"memek",
"tai",
"goblok",
"tolol",
"bego",
"sialan",
"jancuk",
"kampret",
"pepek",
"jembut",
"ngentot",
"ngewe",
"coli",
"celaka",
"laknat",
"pantek",
"entod",
"ndasmu",
"ndas",
"piyo",
"asu",
];
const FALSE_POSITIVE_WHITELISTS: Record<string, string[]> = {
asu: [
"asus", "masuk", "termasuk", "dimasukkan", "memasukkan",
"kasur", "asumsi", "asuransi", "asupan", "pasukan", "pasundan",
"asus",
"masuk",
"termasuk",
"dimasukkan",
"memasukkan",
"kasur",
"asumsi",
"asuransi",
"asupan",
"pasukan",
"pasundan",
],
goblok: ["goblok"],
kontol: ["kontol"],
@@ -199,7 +233,9 @@ async function callNemotronContentSafety(text: string): Promise<string[]> {
* Detect badwords in text using NVIDIA Nemotron-3 Content Safety API.
* Falls back to local lexical list if API key is missing or call fails.
*/
export async function detectIndonesianBadwords(text: string): Promise<string[]> {
export async function detectIndonesianBadwords(
text: string,
): Promise<string[]> {
// Always run local detection first (fast, no network dependency)
const localHits = detectLocalBadwords(text);
@@ -211,7 +247,10 @@ export async function detectIndonesianBadwords(text: string): Promise<string[]>
const allHits = Array.from(new Set([...localHits, ...apiCategories]));
return allHits;
} catch (error) {
log.warn({ error }, "NVIDIA Nemotron API call failed, falling back to local detection");
log.warn(
{ error },
"NVIDIA Nemotron API call failed, falling back to local detection",
);
}
}
@@ -222,7 +261,9 @@ export async function detectIndonesianBadwords(text: string): Promise<string[]>
// Async evidence builders
// ---------------------------------------------------------------------------
export async function buildModerationTextEvidence(text: string): Promise<ModerationTextEvidence> {
export async function buildModerationTextEvidence(
text: string,
): Promise<ModerationTextEvidence> {
const emojiNormalized = normalizeDiscordCustomEmoji(text);
const slangNormalized = normalizeIndonesianSlang(emojiNormalized.text);
const badwordHits = await detectIndonesianBadwords(slangNormalized.text);
@@ -249,7 +290,9 @@ export async function buildModerationTextEvidence(text: string): Promise<Moderat
};
}
export async function formatModerationTextEvidenceForPrompt(text: string): Promise<string> {
export async function formatModerationTextEvidenceForPrompt(
text: string,
): Promise<string> {
const evidence = await buildModerationTextEvidence(text);
if (evidence.normalized === evidence.raw && evidence.notes.length === 0) {
return "";
@@ -257,7 +300,9 @@ export async function formatModerationTextEvidenceForPrompt(text: string): Promi
return [
`[normalized_text: ${evidence.normalized}]`,
evidence.notes.length > 0 ? `[normalization_notes: ${evidence.notes.join("; ")}]` : null,
evidence.notes.length > 0
? `[normalization_notes: ${evidence.notes.join("; ")}]`
: null,
]
.filter(Boolean)
.join(" ");
+5 -1
View File
@@ -34,7 +34,11 @@ export function shouldCaptureMessageLocation(
message: MessageLocationInput,
target: TextCaptureTarget,
): boolean {
if (message.channelId === "1310988070996414494" || message.channelId === "1265679542144467035") return false; // Skip specific channels
if (
message.channelId === "1310988070996414494" ||
message.channelId === "1265679542144467035"
)
return false; // Skip specific channels
if (!message.guildId || message.guildId !== target.guildId) return false;
if (target.channelId && message.channelId !== target.channelId) return false;
return true;
+44 -12
View File
@@ -404,7 +404,9 @@ interface AIAnalysisUpdate {
error?: string | null;
}
function stringifyAIList(value: string[] | string | null | undefined): string | null {
function stringifyAIList(
value: string[] | string | null | undefined,
): string | null {
if (value == null) return null;
return Array.isArray(value) ? JSON.stringify(value) : value;
}
@@ -867,7 +869,9 @@ export async function createMessageReview(
}
}
export async function getMessageReview(id: string): Promise<MessageReview | null> {
export async function getMessageReview(
id: string,
): Promise<MessageReview | null> {
try {
const database = db();
const rows = await database
@@ -878,7 +882,10 @@ export async function getMessageReview(id: string): Promise<MessageReview | null
return (rows[0] as MessageReview) || null;
} catch (error) {
logger.error(
{ reviewId: id, error: error instanceof Error ? error.message : String(error) },
{
reviewId: id,
error: error instanceof Error ? error.message : String(error),
},
"Failed to get message review",
);
throw error;
@@ -918,7 +925,10 @@ export async function listMessageReviews(query: {
.select()
.from(messageReviewsTable)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(messageReviewsTable.created_at), desc(messageReviewsTable.id))
.orderBy(
desc(messageReviewsTable.created_at),
desc(messageReviewsTable.id),
)
.limit(limit + 1);
return pageRows<MessageReview>(rows, limit);
@@ -946,7 +956,10 @@ export async function updateMessageReview(
return rows[0] || null;
} catch (error) {
logger.error(
{ reviewId: id, error: error instanceof Error ? error.message : String(error) },
{
reviewId: id,
error: error instanceof Error ? error.message : String(error),
},
"Failed to update message review",
);
throw error;
@@ -986,7 +999,9 @@ export async function createModerationAction(
}
}
export async function getModerationAction(id: string): Promise<ModerationAction | null> {
export async function getModerationAction(
id: string,
): Promise<ModerationAction | null> {
try {
const database = db();
const rows = await database
@@ -997,7 +1012,10 @@ export async function getModerationAction(id: string): Promise<ModerationAction
return (rows[0] as ModerationAction) || null;
} catch (error) {
logger.error(
{ actionId: id, error: error instanceof Error ? error.message : String(error) },
{
actionId: id,
error: error instanceof Error ? error.message : String(error),
},
"Failed to get moderation action",
);
throw error;
@@ -1033,7 +1051,10 @@ export async function listModerationActions(query: {
.select()
.from(moderationActionsTable)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(desc(moderationActionsTable.created_at), desc(moderationActionsTable.id))
.orderBy(
desc(moderationActionsTable.created_at),
desc(moderationActionsTable.id),
)
.limit(limit + 1);
return pageRows<ModerationAction>(rows, limit);
@@ -1061,7 +1082,10 @@ export async function updateModerationAction(
return rows[0] || null;
} catch (error) {
logger.error(
{ actionId: id, error: error instanceof Error ? error.message : String(error) },
{
actionId: id,
error: error instanceof Error ? error.message : String(error),
},
"Failed to update moderation action",
);
throw error;
@@ -1071,7 +1095,9 @@ export async function updateModerationAction(
// Retention Policies CRUD
// =======================
export async function getRetentionPolicy(guildId: string): Promise<RetentionPolicy | null> {
export async function getRetentionPolicy(
guildId: string,
): Promise<RetentionPolicy | null> {
try {
const database = db();
const rows = await database
@@ -1082,7 +1108,10 @@ export async function getRetentionPolicy(guildId: string): Promise<RetentionPoli
return (rows[0] as RetentionPolicy) || null;
} catch (error) {
logger.error(
{ guildId, error: error instanceof Error ? error.message : String(error) },
{
guildId,
error: error instanceof Error ? error.message : String(error),
},
"Failed to get retention policy",
);
throw error;
@@ -1155,7 +1184,10 @@ export async function getExpiredMessages(
return rows as MessageRecord[];
} catch (error) {
logger.error(
{ retentionDays, error: error instanceof Error ? error.message : String(error) },
{
retentionDays,
error: error instanceof Error ? error.message : String(error),
},
"Failed to get expired messages",
);
throw error;
@@ -21,19 +21,59 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
krn: { normalized: "karena", note: "common abbreviation" },
jgn: { normalized: "jangan", note: "common abbreviation" },
dh: { normalized: "sudah", note: "common abbreviation", safeByDefault: true },
udh: { normalized: "sudah", note: "common abbreviation", safeByDefault: true },
blm: { normalized: "belum", note: "common abbreviation", safeByDefault: true },
sdh: { normalized: "sudah", note: "common abbreviation", safeByDefault: true },
dg: { normalized: "dengan", note: "common abbreviation", safeByDefault: true },
udh: {
normalized: "sudah",
note: "common abbreviation",
safeByDefault: true,
},
blm: {
normalized: "belum",
note: "common abbreviation",
safeByDefault: true,
},
sdh: {
normalized: "sudah",
note: "common abbreviation",
safeByDefault: true,
},
dg: {
normalized: "dengan",
note: "common abbreviation",
safeByDefault: true,
},
dr: { normalized: "dari", note: "common abbreviation", safeByDefault: true },
dlm: { normalized: "dalam", note: "common abbreviation", safeByDefault: true },
dlm: {
normalized: "dalam",
note: "common abbreviation",
safeByDefault: true,
},
gt: { normalized: "gitu", note: "common abbreviation", safeByDefault: true },
doang: { normalized: "doang", note: "Indonesian 'only/just'", safeByDefault: true },
doang: {
normalized: "doang",
note: "Indonesian 'only/just'",
safeByDefault: true,
},
si: { normalized: "si", note: "Indonesian particle", safeByDefault: true },
kah: { normalized: "kah", note: "Indonesian question particle", safeByDefault: true },
ku: { normalized: "aku", note: "first-person informal pronoun", safeByDefault: true },
mu: { normalized: "kamu", note: "second-person informal pronoun suffix", safeByDefault: true },
nya: { normalized: "nya", note: "Indonesian possessive suffix", safeByDefault: true },
kah: {
normalized: "kah",
note: "Indonesian question particle",
safeByDefault: true,
},
ku: {
normalized: "aku",
note: "first-person informal pronoun",
safeByDefault: true,
},
mu: {
normalized: "kamu",
note: "second-person informal pronoun suffix",
safeByDefault: true,
},
nya: {
normalized: "nya",
note: "Indonesian possessive suffix",
safeByDefault: true,
},
// =========================================================================
// Profanity — consonant-dropped / vowelless slang
@@ -41,17 +81,29 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
ajg: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
anjg: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
njing: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
aj: { normalized: "anjing", note: "ultra-short slang for 'anjing' (profanity)" },
aj: {
normalized: "anjing",
note: "ultra-short slang for 'anjing' (profanity)",
},
anj: { normalized: "anjing", note: "short slang for 'anjing' (profanity)" },
anjingg: { normalized: "anjing", note: "elongated 'anjing' (profanity)" },
ajgg: { normalized: "anjing", note: "vowelless + elongated 'anjing' (profanity)" },
ajgg: {
normalized: "anjing",
note: "vowelless + elongated 'anjing' (profanity)",
},
// anjir variants
anjir: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
njir: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
njr: { normalized: "anjing", note: "vowelless 'anjir' (profanity)" },
anjay: { normalized: "anjing", note: "slang 'anjay' (profanity-adjacent)" },
bjir: { normalized: "anjing", note: "slang interjection 'bjir' (profanity-adjacent)" },
bjirr: { normalized: "anjing", note: "elongated 'bjir' (profanity-adjacent)" },
bjir: {
normalized: "anjing",
note: "slang interjection 'bjir' (profanity-adjacent)",
},
bjirr: {
normalized: "anjing",
note: "elongated 'bjir' (profanity-adjacent)",
},
// bangsat variants
bgsd: { normalized: "bangsat", note: "slang for 'bangsat' (profanity)" },
bgst: { normalized: "bangsat", note: "slang for 'bangsat' (profanity)" },
@@ -91,8 +143,14 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
sln: { normalized: "sialan", note: "slang for 'sialan' (profanity)" },
sialann: { normalized: "sialan", note: "elongated 'sialan' (profanity)" },
// jancuk (Javanese)
jncuk: { normalized: "jancuk", note: "Javanese slang for 'jancuk' (profanity)" },
jcuk: { normalized: "jancuk", note: "Javanese slang for 'jancuk' (profanity)" },
jncuk: {
normalized: "jancuk",
note: "Javanese slang for 'jancuk' (profanity)",
},
jcuk: {
normalized: "jancuk",
note: "Javanese slang for 'jancuk' (profanity)",
},
jncukk: { normalized: "jancuk", note: "elongated 'jancuk' (profanity)" },
// kampret
kmprt: { normalized: "kampret", note: "slang for 'kampret' (profanity)" },
@@ -118,7 +176,10 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
// entod
ntd: { normalized: "entod", note: "slang for 'entod' (profanity)" },
// Javanese insults
ndasmu: { normalized: "ndasmu", note: "Javanese insult 'ndasmu' (profanity)" },
ndasmu: {
normalized: "ndasmu",
note: "Javanese insult 'ndasmu' (profanity)",
},
ndas: { normalized: "ndas", note: "Javanese insult 'ndas' (profanity)" },
// piyo (Javanese profanity)
piyoo: { normalized: "piyo", note: "Javanese slang 'piyo' (profanity)" },
@@ -135,31 +196,67 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
// furry subculture
furry: { normalized: "furry", note: "furry / prohibited topic" },
furries: { normalized: "furry", note: "furry / prohibited topic" },
transfurry: { normalized: "transfurry", note: "transfurry / prohibited topic" },
transfur: { normalized: "transfurry", note: "slang for 'transfurry' / prohibited topic" },
protogen: { normalized: "protogen", note: "furry subculture / prohibited topic" },
transfurry: {
normalized: "transfurry",
note: "transfurry / prohibited topic",
},
transfur: {
normalized: "transfurry",
note: "slang for 'transfurry' / prohibited topic",
},
protogen: {
normalized: "protogen",
note: "furry subculture / prohibited topic",
},
therian: { normalized: "therian", note: "therianthropy / prohibited topic" },
therianthropy: { normalized: "therianthropy", note: "therianthropy / prohibited topic" },
otherkin: { normalized: "otherkin", note: "otherkin identity / prohibited topic" },
therianthropy: {
normalized: "therianthropy",
note: "therianthropy / prohibited topic",
},
otherkin: {
normalized: "otherkin",
note: "otherkin identity / prohibited topic",
},
// furry-adjacent terms
yiff: { normalized: "yiff", note: "furry sexual content / prohibited topic" },
fursona: { normalized: "fursona", note: "furry persona / prohibited topic" },
fursonas: { normalized: "fursona", note: "furry personas / prohibited topic" },
fursonas: {
normalized: "fursona",
note: "furry personas / prohibited topic",
},
fursuit: { normalized: "fursuit", note: "furry costume / prohibited topic" },
fursuits: { normalized: "fursuit", note: "furry costumes / prohibited topic" },
fursuits: {
normalized: "fursuit",
note: "furry costumes / prohibited topic",
},
// sexual orientation terms
gayy: { normalized: "gay", note: "elongated 'gay' / prohibited topic" },
lesbi: { normalized: "lesbian", note: "lesbian / prohibited topic" },
lesbii: { normalized: "lesbian", note: "slang for 'lesbian' / prohibited topic" },
lesbii: {
normalized: "lesbian",
note: "slang for 'lesbian' / prohibited topic",
},
homo: { normalized: "homo", note: "homosexual slur / prohibited topic" },
waria: { normalized: "waria", note: "waria / prohibited topic" },
trans: { normalized: "transgender", note: "transgender / prohibited topic" },
nonbinary: { normalized: "nonbinary", note: "nonbinary identity / prohibited topic" },
nb: { normalized: "nonbinary", note: "nonbinary abbreviation / prohibited topic" },
genderfluid: { normalized: "genderfluid", note: "genderfluid / prohibited topic" },
nonbinary: {
normalized: "nonbinary",
note: "nonbinary identity / prohibited topic",
},
nb: {
normalized: "nonbinary",
note: "nonbinary abbreviation / prohibited topic",
},
genderfluid: {
normalized: "genderfluid",
note: "genderfluid / prohibited topic",
},
pansexual: { normalized: "pansexual", note: "pansexual / prohibited topic" },
asexual: { normalized: "asexual", note: "asexual / prohibited topic" },
ace: { normalized: "asexual", note: "asexual abbreviation / prohibited topic" },
ace: {
normalized: "asexual",
note: "asexual abbreviation / prohibited topic",
},
enby: { normalized: "enby", note: "NB/nonbinary slang / prohibited topic" },
// =========================================================================
@@ -448,8 +545,14 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
s4s: { normalized: "sub4sub", note: "spam engagement abbreviation" },
like4like: { normalized: "like4like", note: "spam engagement" },
l4l: { normalized: "like4like", note: "spam engagement abbreviation" },
dm: { normalized: "DM", note: "direct message — check for spam/scam context" },
pm: { normalized: "PM", note: "private message — check for spam/scam context" },
dm: {
normalized: "DM",
note: "direct message — check for spam/scam context",
},
pm: {
normalized: "PM",
note: "private message — check for spam/scam context",
},
click: { normalized: "click", note: "potential clickbait/scam" },
link: { normalized: "link", note: "potential spam link — check context" },
free: { normalized: "free", note: "potential spam bait — check context" },
@@ -468,15 +571,24 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
narkoba: { normalized: "narkoba", note: "narcotics / prohibited topic" },
kokain: { normalized: "kokain", note: "cocaine / prohibited topic" },
ekstasi: { normalized: "ekstasi", note: "ecstasy / prohibited topic" },
shabu: { normalized: "sabu", note: "variant spelling 'sabu' / prohibited topic" },
shabu: {
normalized: "sabu",
note: "variant spelling 'sabu' / prohibited topic",
},
// =========================================================================
// Violence / threat indicators
// =========================================================================
bunuh: { normalized: "bunuh", note: "kill / violence indicator" },
bunuhdiri: { normalized: "bunuh diri", note: "suicide / self-harm indicator" },
bunuhdiri: {
normalized: "bunuh diri",
note: "suicide / self-harm indicator",
},
mati: { normalized: "mati", note: "die / death — context-dependent" },
matiin: { normalized: "matikan", note: "turn off / kill — context-dependent" },
matiin: {
normalized: "matikan",
note: "turn off / kill — context-dependent",
},
ancam: { normalized: "ancam", note: "threat indicator" },
ancamn: { normalized: "ancaman", note: "threat indicator" },
bakar: { normalized: "bakar", note: "burn / violence indicator" },
@@ -586,7 +698,11 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
porn: { normalized: "porno", note: "pornography / NSFW content" },
porno: { normalized: "porno", note: "pornography / NSFW content" },
bokep: { normalized: "bokep", note: "pornography / NSFW content" },
bokap: { normalized: "bokap", note: "'father' slang — NOT bokep", safeByDefault: true },
bokap: {
normalized: "bokap",
note: "'father' slang — NOT bokep",
safeByDefault: true,
},
ngeseks: { normalized: "ngeseks", note: "having sex / NSFW content" },
masturbasi: { normalized: "masturbasi", note: "masturbation / NSFW content" },
onani: { normalized: "onani", note: "masturbation / NSFW content" },
@@ -634,8 +750,14 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
// =========================================================================
// Money / financial scam indicators
// =========================================================================
investment: { normalized: "investment", note: "potential investment scam — check context" },
investasi: { normalized: "investasi", note: "potential investment scam — check context" },
investment: {
normalized: "investment",
note: "potential investment scam — check context",
},
investasi: {
normalized: "investasi",
note: "potential investment scam — check context",
},
crypto: { normalized: "crypto", note: "crypto scam — check context" },
bitcoin: { normalized: "bitcoin", note: "crypto — check context" },
btc: { normalized: "bitcoin", note: "crypto — check context" },
@@ -650,8 +772,17 @@ export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
claim: { normalized: "claim", note: "scam bait — check context" },
klaim: { normalized: "klaim", note: "scam bait — check context" },
verify: { normalized: "verify", note: "potential phishing — check context" },
verifikasi: { normalized: "verifikasi", note: "potential phishing — check context" },
verifikasi: {
normalized: "verifikasi",
note: "potential phishing — check context",
},
wallet: { normalized: "wallet", note: "crypto wallet — check context" },
seed: { normalized: "seed phrase", note: "crypto seed phrase — check context" },
recovery: { normalized: "recovery phrase", note: "crypto recovery — check context" },
seed: {
normalized: "seed phrase",
note: "crypto seed phrase — check context",
},
recovery: {
normalized: "recovery phrase",
note: "crypto recovery — check context",
},
};
+8 -6
View File
@@ -6,10 +6,7 @@ import {
voiceRecordingsTable,
} from "../database/schema.js";
import { createChildLogger } from "../logger.js";
import {
getExpiredMessages,
getRetentionPolicy,
} from "./messageStore.js";
import { getExpiredMessages, getRetentionPolicy } from "./messageStore.js";
import type { RetentionPolicy } from "./types.js";
import { and, eq, isNull, lt, sql } from "drizzle-orm";
@@ -152,7 +149,10 @@ export async function executeAllRetentionPolicies(): Promise<{
return summary;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error({ error: message }, "Failed to execute all retention policies");
logger.error(
{ error: message },
"Failed to execute all retention policies",
);
throw error;
}
}
@@ -161,7 +161,9 @@ export async function executeAllRetentionPolicies(): Promise<{
* Starts a periodic retention policy executor
* Runs every 24 hours by default
*/
export function startRetentionPolicyWorker(intervalMs: number = 24 * 60 * 60 * 1000): NodeJS.Timeout {
export function startRetentionPolicyWorker(
intervalMs: number = 24 * 60 * 60 * 1000,
): NodeJS.Timeout {
logger.info({ intervalMs }, "Starting retention policy worker");
const interval = setInterval(async () => {
+4 -2
View File
@@ -66,7 +66,8 @@ async function isSafeUrl(urlStr: string): Promise<boolean> {
function extractOgImage(html: string): string | null {
// Look for <meta ... property="og:image" ... content="..."> or <meta ... name="twitter:image" ... content="...">
const ogRegex = /<meta[^>]*(?:property|name)=["'](?:og:image|twitter:image)["'][^>]*content=["']([^"']+)["']/i;
const ogRegex =
/<meta[^>]*(?:property|name)=["'](?:og:image|twitter:image)["'][^>]*content=["']([^"']+)["']/i;
const match = html.match(ogRegex);
if (match && match[1]) {
// Unescape basic HTML entities
@@ -74,7 +75,8 @@ function extractOgImage(html: string): string | null {
}
// Try reversed attribute order: <meta ... content="..." ... property="og:image">
const ogRegexRev = /<meta[^>]*content=["']([^"']+)["'][^>]*(?:property|name)=["'](?:og:image|twitter:image)["']/i;
const ogRegexRev =
/<meta[^>]*content=["']([^"']+)["'][^>]*(?:property|name)=["'](?:og:image|twitter:image)["']/i;
const matchRev = html.match(ogRegexRev);
if (matchRev && matchRev[1]) {
return matchRev[1].replace(/&amp;/g, "&").replace(/&quot;/g, '"');
+6 -32
View File
@@ -17,11 +17,7 @@ export function createAnalyticsRoutes(): Router {
// Query params: guildId (required), channelId, hours (default 24)
router.get("/analytics/overview", async (req, res, next) => {
try {
const {
guildId,
channelId,
hours,
} = req.query as {
const { guildId, channelId, hours } = req.query as {
guildId?: string;
channelId?: string;
hours?: string;
@@ -53,11 +49,7 @@ export function createAnalyticsRoutes(): Router {
// Query params: guildId (required), channelId, hours (default 24)
router.get("/analytics/hourly", async (req, res, next) => {
try {
const {
guildId,
channelId,
hours,
} = req.query as {
const { guildId, channelId, hours } = req.query as {
guildId?: string;
channelId?: string;
hours?: string;
@@ -89,11 +81,7 @@ export function createAnalyticsRoutes(): Router {
// Query params: guildId (required), channelId, hours (default 24)
router.get("/analytics/topics", async (req, res, next) => {
try {
const {
guildId,
channelId,
hours,
} = req.query as {
const { guildId, channelId, hours } = req.query as {
guildId?: string;
channelId?: string;
hours?: string;
@@ -125,12 +113,7 @@ export function createAnalyticsRoutes(): Router {
// Query params: guildId (required), channelId, hours (default 24), limit (default 20)
router.get("/analytics/leaderboard", async (req, res, next) => {
try {
const {
guildId,
channelId,
hours,
limit,
} = req.query as {
const { guildId, channelId, hours, limit } = req.query as {
guildId?: string;
channelId?: string;
hours?: string;
@@ -165,11 +148,7 @@ export function createAnalyticsRoutes(): Router {
// Query params: guildId (required), channelId, hours (default 24)
router.get("/analytics/stats", async (req, res, next) => {
try {
const {
guildId,
channelId,
hours,
} = req.query as {
const { guildId, channelId, hours } = req.query as {
guildId?: string;
channelId?: string;
hours?: string;
@@ -201,12 +180,7 @@ export function createAnalyticsRoutes(): Router {
// Query params: guildId (required), channelId, hours (default 24), limit (default 20)
router.get("/analytics/violators", async (req, res, next) => {
try {
const {
guildId,
channelId,
hours,
limit,
} = req.query as {
const { guildId, channelId, hours, limit } = req.query as {
guildId?: string;
channelId?: string;
hours?: string;