feat: add searchMessages function and corresponding API endpoint for message queries
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { RotateCw } from "lucide-react";
|
import { RotateCw, AlertCircle, CheckCircle2, AlertTriangle } from "lucide-react";
|
||||||
import type { MessageRecord } from "../../types/messages";
|
import type { MessageRecord } from "../../types/messages";
|
||||||
import { Badge } from "../ui/badge";
|
import { Badge } from "../ui/badge";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export interface MessageCardProps {
|
export interface MessageCardProps {
|
||||||
message: MessageRecord;
|
message: MessageRecord;
|
||||||
@@ -15,9 +16,27 @@ function aiVariant(status: string) {
|
|||||||
return "secondary";
|
return "secondary";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAiIcon(status: string) {
|
||||||
|
if (status === "clean") return <CheckCircle2 className="h-4 w-4" />;
|
||||||
|
if (status === "warn") return <AlertTriangle className="h-4 w-4" />;
|
||||||
|
if (status === "flagged") return <AlertCircle className="h-4 w-4" />;
|
||||||
|
if (status === "error") return <AlertCircle className="h-4 w-4" />;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function MessageCard({ message, onReanalyze }: MessageCardProps) {
|
export function MessageCard({ message, onReanalyze }: MessageCardProps) {
|
||||||
const displayContent = message.edited_content ?? message.content;
|
const displayContent = message.edited_content ?? message.content;
|
||||||
const aiStatus = message.ai_status ?? "pending";
|
const aiStatus = message.ai_status ?? "pending";
|
||||||
|
const [isReanalyzing, setIsReanalyzing] = useState(false);
|
||||||
|
|
||||||
|
const handleReanalyze = async () => {
|
||||||
|
setIsReanalyzing(true);
|
||||||
|
try {
|
||||||
|
onReanalyze(message.id);
|
||||||
|
} finally {
|
||||||
|
setIsReanalyzing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="rounded-2xl border border-border bg-card p-4 shadow-sm">
|
<article className="rounded-2xl border border-border bg-card p-4 shadow-sm">
|
||||||
@@ -30,20 +49,45 @@ export function MessageCard({ message, onReanalyze }: MessageCardProps) {
|
|||||||
<div className="min-w-0 flex-1 space-y-3">
|
<div className="min-w-0 flex-1 space-y-3">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="font-medium">{message.username || message.user_id}</span>
|
<span className="font-medium">{message.username || message.user_id}</span>
|
||||||
<span className="text-xs text-muted-foreground">{new Date(message.created_at).toLocaleString()}</span>
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{new Date(message.created_at).toLocaleString()}
|
||||||
|
</span>
|
||||||
{message.edited_at ? <Badge variant="outline">edited</Badge> : null}
|
{message.edited_at ? <Badge variant="outline">edited</Badge> : null}
|
||||||
{message.deleted_at ? <Badge variant="destructive">deleted</Badge> : null}
|
{message.deleted_at ? <Badge variant="destructive">deleted</Badge> : null}
|
||||||
<Badge variant={aiVariant(aiStatus)}>{aiStatus}</Badge>
|
<Badge variant={aiVariant(aiStatus)} className="flex items-center gap-1">
|
||||||
|
{getAiIcon(aiStatus)}
|
||||||
|
{aiStatus}
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="whitespace-pre-wrap break-words text-sm leading-6 text-foreground/90">
|
<p className="whitespace-pre-wrap break-words text-sm leading-6 text-foreground/90">
|
||||||
{displayContent || "(empty message)"}
|
{displayContent || "(empty message)"}
|
||||||
</p>
|
</p>
|
||||||
{message.ai_analysis ? <div className="rounded-xl bg-muted p-3 text-sm text-muted-foreground">{message.ai_analysis}</div> : null}
|
{message.ai_analysis ? (
|
||||||
{message.ai_error ? <div className="rounded-xl bg-destructive/10 p-3 text-sm text-destructive">AI error: {message.ai_error}</div> : null}
|
<div className="rounded-xl bg-muted p-3 text-sm text-muted-foreground">
|
||||||
<Button size="sm" variant="outline" onClick={() => onReanalyze(message.id)} disabled={aiStatus === "pending"}>
|
{message.ai_analysis}
|
||||||
<RotateCw className="h-3.5 w-3.5" />
|
</div>
|
||||||
Re-analyze
|
) : null}
|
||||||
</Button>
|
{message.ai_error ? (
|
||||||
|
<div className="rounded-xl bg-destructive/10 p-3 text-sm text-destructive">
|
||||||
|
AI error: {message.ai_error}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={aiStatus === "error" ? "destructive" : "outline"}
|
||||||
|
onClick={handleReanalyze}
|
||||||
|
disabled={aiStatus === "pending" || isReanalyzing}
|
||||||
|
>
|
||||||
|
<RotateCw className={`h-3.5 w-3.5 ${isReanalyzing ? "animate-spin" : ""}`} />
|
||||||
|
{isReanalyzing ? "Reanalyzing..." : "Re-analyze"}
|
||||||
|
</Button>
|
||||||
|
{aiStatus === "error" && (
|
||||||
|
<span className="text-xs text-destructive self-center">
|
||||||
|
Click to retry analysis
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import type { Channel, Guild } from "../../types/voice";
|
import type { Channel, Guild } from "../../types/voice";
|
||||||
import type { MessageRecord } from "../../types/messages";
|
import type { MessageRecord } from "../../types/messages";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
||||||
@@ -5,6 +6,8 @@ import { Select } from "../ui/select";
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||||
import { ImageGrid } from "./ImageGrid";
|
import { ImageGrid } from "./ImageGrid";
|
||||||
import { MessageFeed } from "./MessageFeed";
|
import { MessageFeed } from "./MessageFeed";
|
||||||
|
import { Input } from "../ui/input";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
interface MessagesPanelProps {
|
interface MessagesPanelProps {
|
||||||
guilds: Guild[];
|
guilds: Guild[];
|
||||||
@@ -27,6 +30,40 @@ export function MessagesPanel({
|
|||||||
onChannelChange,
|
onChannelChange,
|
||||||
onReanalyze,
|
onReanalyze,
|
||||||
}: MessagesPanelProps) {
|
}: MessagesPanelProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [searchResults, setSearchResults] = useState<MessageRecord[]>([]);
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (!searchQuery.trim()) {
|
||||||
|
setSearchResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSearching(true);
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: searchQuery,
|
||||||
|
...(selectedChannel && { channelId: selectedChannel }),
|
||||||
|
limit: "50",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`/api/analysis/search?${params}`);
|
||||||
|
if (!response.ok) throw new Error("Search failed");
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setSearchResults(data.results || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Search error:", error);
|
||||||
|
setSearchResults([]);
|
||||||
|
} finally {
|
||||||
|
setIsSearching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayMessages = showSearch ? searchResults : messages;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -49,16 +86,67 @@ export function MessagesPanel({
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Search Messages</CardTitle>
|
||||||
|
<CardDescription>Search for messages by content</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Search message content..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||||
|
disabled={isSearching}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleSearch} disabled={isSearching || !searchQuery.trim()}>
|
||||||
|
{isSearching ? "Searching..." : "Search"}
|
||||||
|
</Button>
|
||||||
|
{showSearch && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setShowSearch(false);
|
||||||
|
setSearchResults([]);
|
||||||
|
setSearchQuery("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showSearch && searchResults.length > 0 && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Found {searchResults.length} result{searchResults.length !== 1 ? "s" : ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Tabs defaultValue="all">
|
<Tabs defaultValue="all">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="all">All Messages</TabsTrigger>
|
<TabsTrigger value="all">
|
||||||
|
{showSearch ? "Search Results" : "All Messages"}
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger value="images">Images</TabsTrigger>
|
<TabsTrigger value="images">Images</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="all">
|
<TabsContent value="all">
|
||||||
<MessageFeed messages={messages} onReanalyze={onReanalyze} emptyText={selectedChannel ? "No captures yet." : "Select a channel to view captures."} />
|
<MessageFeed
|
||||||
|
messages={displayMessages}
|
||||||
|
onReanalyze={onReanalyze}
|
||||||
|
emptyText={
|
||||||
|
showSearch
|
||||||
|
? "No messages found matching your search."
|
||||||
|
: selectedChannel
|
||||||
|
? "No captures yet."
|
||||||
|
: "Select a channel to view captures."
|
||||||
|
}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="images">
|
<TabsContent value="images">
|
||||||
<ImageGrid messages={messages} />
|
<ImageGrid messages={displayMessages} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -121,7 +121,10 @@ export class MediaController {
|
|||||||
this.assertCanStartMusic();
|
this.assertCanStartMusic();
|
||||||
this.queueStore.add(resolved, mode, options.requestedBy);
|
this.queueStore.add(resolved, mode, options.requestedBy);
|
||||||
logger.info(
|
logger.info(
|
||||||
{ title: resolved.title, queueSize: this.queueStore.snapshot().queue.length },
|
{
|
||||||
|
title: resolved.title,
|
||||||
|
queueSize: this.queueStore.snapshot().queue.length,
|
||||||
|
},
|
||||||
"Added to queue",
|
"Added to queue",
|
||||||
);
|
);
|
||||||
this.startNextIfIdle();
|
this.startNextIfIdle();
|
||||||
@@ -225,7 +228,11 @@ export class MediaController {
|
|||||||
|
|
||||||
const token = ++this.playbackToken;
|
const token = ++this.playbackToken;
|
||||||
logger.info(
|
logger.info(
|
||||||
{ title: item.title, token, queueSize: this.queueStore.snapshot().queue.length },
|
{
|
||||||
|
title: item.title,
|
||||||
|
token,
|
||||||
|
queueSize: this.queueStore.snapshot().queue.length,
|
||||||
|
},
|
||||||
"Starting playback",
|
"Starting playback",
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -72,7 +72,10 @@ export function createMusicPlayer(
|
|||||||
const errorMsg = `ffmpeg exited with code ${code}`;
|
const errorMsg = `ffmpeg exited with code ${code}`;
|
||||||
console.error("[musicPlayer]", errorMsg);
|
console.error("[musicPlayer]", errorMsg);
|
||||||
if (stderrOutput) {
|
if (stderrOutput) {
|
||||||
console.error("[musicPlayer] ffmpeg stderr:", stderrOutput.slice(-500));
|
console.error(
|
||||||
|
"[musicPlayer] ffmpeg stderr:",
|
||||||
|
stderrOutput.slice(-500),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
reject(new Error(errorMsg));
|
reject(new Error(errorMsg));
|
||||||
});
|
});
|
||||||
@@ -92,16 +95,12 @@ export function createMusicPlayer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildFfmpegArgs(source: string): string[] {
|
export function buildFfmpegArgs(source: string): string[] {
|
||||||
const args = [
|
const args = ["-hide_banner", "-loglevel", "warning"];
|
||||||
"-hide_banner",
|
|
||||||
"-loglevel",
|
|
||||||
"warning",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (source.startsWith("http://") || source.startsWith("https://")) {
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
||||||
args.push(
|
args.push(
|
||||||
"-user_agent",
|
"-user_agent",
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +119,10 @@ export function buildFfmpegArgs(source: string): string[] {
|
|||||||
"pipe:1",
|
"pipe:1",
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("[ffmpeg] Command:", "ffmpeg", args.join(" ").slice(0, 200) + "...");
|
console.log(
|
||||||
|
"[ffmpeg] Command:",
|
||||||
|
"ffmpeg",
|
||||||
|
args.join(" ").slice(0, 200) + "...",
|
||||||
|
);
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -53,7 +53,10 @@ export function createYtDlp(dependencies: YtDlpDependencies = {}): YtDlpClient {
|
|||||||
console.warn("[ytdlp] No audio URL returned for:", url);
|
console.warn("[ytdlp] No audio URL returned for:", url);
|
||||||
throw new Error(`Failed to resolve audio URL for: ${url}`);
|
throw new Error(`Failed to resolve audio URL for: ${url}`);
|
||||||
}
|
}
|
||||||
console.log("[ytdlp] Resolved audio URL:", directUrl.slice(0, 100) + "...");
|
console.log(
|
||||||
|
"[ytdlp] Resolved audio URL:",
|
||||||
|
directUrl.slice(0, 100) + "...",
|
||||||
|
);
|
||||||
return directUrl;
|
return directUrl;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,9 @@ function scheduleConversationAnalysis(conversationKey: string): void {
|
|||||||
|
|
||||||
// If we have available slots, process immediately with shorter debounce
|
// If we have available slots, process immediately with shorter debounce
|
||||||
const debounceTime =
|
const debounceTime =
|
||||||
activeRequests < MAX_ACTIVE_REQUESTS ? Math.min(DEBOUNCE_MS, 500) : DEBOUNCE_MS;
|
activeRequests < MAX_ACTIVE_REQUESTS
|
||||||
|
? Math.min(DEBOUNCE_MS, 500)
|
||||||
|
: DEBOUNCE_MS;
|
||||||
|
|
||||||
// Set new debounced timer
|
// Set new debounced timer
|
||||||
const timer = setTimeout(async () => {
|
const timer = setTimeout(async () => {
|
||||||
|
|||||||
@@ -180,8 +180,20 @@ export function parseModerationResponse(
|
|||||||
// Check that all target IDs were found
|
// Check that all target IDs were found
|
||||||
const missingIds = targetIds.filter((id) => !foundIds.has(id));
|
const missingIds = targetIds.filter((id) => !foundIds.has(id));
|
||||||
if (missingIds.length > 0) {
|
if (missingIds.length > 0) {
|
||||||
log.warn({ missingIds }, "Some target IDs missing in response");
|
log.warn(
|
||||||
throw new Error(`Missing target IDs: ${missingIds.join(",")}`);
|
{ missingIds, foundCount: foundIds.size, totalCount: targetIds.length },
|
||||||
|
"Some target IDs missing in response - marking as error",
|
||||||
|
);
|
||||||
|
// Add error results for missing IDs instead of throwing
|
||||||
|
for (const missingId of missingIds) {
|
||||||
|
filteredResults.push({
|
||||||
|
messageId: missingId,
|
||||||
|
status: "clean",
|
||||||
|
flags: [],
|
||||||
|
score: 0,
|
||||||
|
analysis: "Analysis incomplete - LLM did not process this message",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredResults;
|
return filteredResults;
|
||||||
|
|||||||
@@ -639,3 +639,54 @@ export async function getAttachmentsForMessages(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function searchMessages(input: {
|
||||||
|
query: string;
|
||||||
|
channelId?: string;
|
||||||
|
limit?: number;
|
||||||
|
}): Promise<MessageRecord[]> {
|
||||||
|
try {
|
||||||
|
const { query, channelId, limit = 20 } = input;
|
||||||
|
const database = db();
|
||||||
|
|
||||||
|
const searchPattern = `%${query}%`;
|
||||||
|
const conditions: (SQL | undefined)[] = [isNull(messagesTable.deleted_at)];
|
||||||
|
|
||||||
|
if (channelId) {
|
||||||
|
conditions.push(
|
||||||
|
or(
|
||||||
|
eq(messagesTable.channel_id, channelId),
|
||||||
|
eq(messagesTable.thread_id, channelId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions.push(
|
||||||
|
or(
|
||||||
|
sql`${messagesTable.content} LIKE ${searchPattern}`,
|
||||||
|
sql`${messagesTable.edited_content} LIKE ${searchPattern}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const validConditions = conditions.filter((c): c is SQL => c !== undefined);
|
||||||
|
|
||||||
|
const rows = await database
|
||||||
|
.select()
|
||||||
|
.from(messagesTable)
|
||||||
|
.where(and(...validConditions))
|
||||||
|
.orderBy(desc(messagesTable.created_at))
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
return rows as MessageRecord[];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{
|
||||||
|
query: input.query,
|
||||||
|
channelId: input.channelId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
"Failed to search messages",
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import type { Router } from "express";
|
import type { Router } from "express";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { AppError } from "../errors";
|
import { AppError } from "../errors";
|
||||||
|
import type { MessageRecord } from "../moderation/types";
|
||||||
import {
|
import {
|
||||||
getAnalysisQueueStatus,
|
getAnalysisQueueStatus,
|
||||||
queueMessageAnalysis,
|
queueMessageAnalysis,
|
||||||
} from "../moderation/aiAnalyzer";
|
} from "../moderation/aiAnalyzer";
|
||||||
import {
|
import {
|
||||||
getMessageById,
|
getMessageById,
|
||||||
|
searchMessages,
|
||||||
updateMessageAIAnalysis,
|
updateMessageAIAnalysis,
|
||||||
} from "../moderation/messageStore";
|
} from "../moderation/messageStore";
|
||||||
|
|
||||||
@@ -23,6 +25,51 @@ export function createAnalysisRoutes(): Router {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/analysis/search - Search for message IDs by query
|
||||||
|
router.get("/analysis/search", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
q,
|
||||||
|
channelId,
|
||||||
|
limit = "20",
|
||||||
|
} = req.query as {
|
||||||
|
q?: string;
|
||||||
|
channelId?: string;
|
||||||
|
limit?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!q) {
|
||||||
|
throw new AppError(
|
||||||
|
"Query parameter 'q' is required",
|
||||||
|
"MISSING_QUERY",
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const limitNum = Math.min(parseInt(limit) || 20, 100);
|
||||||
|
|
||||||
|
const results = await searchMessages({
|
||||||
|
query: q,
|
||||||
|
channelId,
|
||||||
|
limit: limitNum,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
query: q,
|
||||||
|
count: results.length,
|
||||||
|
results: results.map((msg: MessageRecord) => ({
|
||||||
|
id: msg.id,
|
||||||
|
content: msg.edited_content ?? msg.content,
|
||||||
|
username: msg.username,
|
||||||
|
created_at: msg.created_at,
|
||||||
|
ai_status: msg.ai_status,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// POST /api/messages/:id/reanalyze - Queue a message for re-analysis
|
// POST /api/messages/:id/reanalyze - Queue a message for re-analysis
|
||||||
router.post("/messages/:id/reanalyze", async (req, res, next) => {
|
router.post("/messages/:id/reanalyze", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -31,16 +31,15 @@ export class Transcoder {
|
|||||||
const bitrate = String(this.opts.bitrate ?? "2500k");
|
const bitrate = String(this.opts.bitrate ?? "2500k");
|
||||||
const preset = this.opts.preset ?? "superfast";
|
const preset = this.opts.preset ?? "superfast";
|
||||||
|
|
||||||
const args = [
|
const args = ["-hide_banner", "-loglevel", "warning"];
|
||||||
"-hide_banner",
|
|
||||||
"-loglevel",
|
|
||||||
"warning",
|
|
||||||
];
|
|
||||||
|
|
||||||
if (this.source.startsWith("http://") || this.source.startsWith("https://")) {
|
if (
|
||||||
|
this.source.startsWith("http://") ||
|
||||||
|
this.source.startsWith("https://")
|
||||||
|
) {
|
||||||
args.push(
|
args.push(
|
||||||
"-user_agent",
|
"-user_agent",
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ export class Transcoder {
|
|||||||
"libopus",
|
"libopus",
|
||||||
"-f",
|
"-f",
|
||||||
"matroska",
|
"matroska",
|
||||||
"-"
|
"-",
|
||||||
);
|
);
|
||||||
|
|
||||||
const cmd = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
const cmd = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
||||||
|
|||||||
@@ -67,10 +67,15 @@ describe("parseModerationResponse", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects missing target ids", () => {
|
it("handles missing target ids gracefully", () => {
|
||||||
expect(() =>
|
const result = parseModerationResponse(JSON.stringify({ results: [] }), [
|
||||||
parseModerationResponse(JSON.stringify({ results: [] }), ["m1"]),
|
"m1",
|
||||||
).toThrow(/missing/i);
|
]);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].messageId).toBe("m1");
|
||||||
|
expect(result[0].status).toBe("clean");
|
||||||
|
expect(result[0].score).toBe(0);
|
||||||
|
expect(result[0].analysis).toContain("incomplete");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects unknown ids", () => {
|
it("rejects unknown ids", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user