fix: backend and discord-gateway improvements

- Update shared database schema
- Add shared utils
- Refactor backend middleware, auth routes, and dashboard repository
- Improve media analysis client with better error handling
- Fix searxng search URL construction
- Update URL fetcher for robustness

Co-authored-by: workflow agents
This commit is contained in:
asepharyana
2026-07-02 06:02:07 +07:00
co-authored by workflow agents
parent 81a004d250
commit ade5d6a7c3
9 changed files with 174 additions and 53 deletions
@@ -11,7 +11,7 @@ import { readFile, writeFile, unlink, rm, mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { delay } from "@bete/shared/utils";
import { createAbortControllerWithTimeout, delay } from "@bete/shared/utils";
import { LRUCache } from "lru-cache";
import { config } from "../../shared/config/config.js";
import { resizeImageForVision } from "../attachment-upload/imageResizer.js";
@@ -297,8 +297,7 @@ async function downloadSingleAttachment(
const urlToUse = att.uploaded_url ?? att.discord_url ?? null;
if (!urlToUse) return;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);
const { controller, clear } = createAbortControllerWithTimeout(15000);
try {
const res = await fetch(urlToUse, { signal: controller.signal });
if (!res.ok || !res.body) return;
@@ -322,7 +321,35 @@ async function downloadSingleAttachment(
await extractVideoFrames(att, imageBytes, targetId, maxDimension, imageMap);
return;
}
if (!sniffedMime) return;
// Fallback: try attachment type metadata, then filename extension
let resolvedMime = sniffedMime;
if (!resolvedMime) {
if (att.type.startsWith("image/")) {
resolvedMime = att.type;
log.warn({ attachmentId: att.id, filename: att.filename, type: att.type },
"Image MIME sniff failed — using attachment metadata type as fallback");
} else {
// Last resort: check file extension
const ext = att.filename?.toLowerCase().split(".").pop();
if (ext && ["jpg", "jpeg", "png", "gif", "webp", "bmp"].includes(ext)) {
const mimeMap: Record<string, string> = {
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png",
gif: "image/gif", webp: "image/webp", bmp: "image/bmp",
};
resolvedMime = mimeMap[ext];
log.warn({ attachmentId: att.id, filename: att.filename, ext },
"Image MIME sniff failed — using file extension fallback");
}
}
}
// If all fallbacks fail, still try with generic image/jpeg (better than silent skip)
if (!resolvedMime) {
resolvedMime = "image/jpeg";
log.warn({ attachmentId: att.id, filename: att.filename },
"All MIME detection failed — forcing image/jpeg as last resort");
}
const { data: resizedBuffer, mimeType: resizedMime } = await resizeImageForVision(imageBytes, maxDimension);
const dataUrl = `data:${resizedMime};base64,${resizedBuffer.toString("base64")}`;
@@ -334,7 +361,7 @@ async function downloadSingleAttachment(
} catch (err) {
log.warn({ attachmentId: att.id, error: err instanceof Error ? err.message : String(err) }, "Download failed");
} finally {
clearTimeout(timeoutId);
clear();
}
}
@@ -404,6 +431,8 @@ async function downloadMediaCandidate(
const existing = mediaAnalysisMap.get(targetId) ?? [];
existing.push(`[Media analysis for message ${candidate.messageId}] ${candidate.label}: ${cached}`);
mediaAnalysisMap.set(targetId, existing);
// Warm the LRU cache so subsequent calls in the same process skip DB query
visionLruCache.set(vck, cached);
return;
}
}
@@ -1,5 +1,6 @@
import Redis from "ioredis";
import { createChildLogger } from "@bete/shared/logger";
import { createAbortControllerWithTimeout } from "@bete/shared/utils";
const log = createChildLogger("searxng-search");
@@ -68,43 +69,45 @@ export async function searchSearxng(
// Cache miss — hit SearXNG API
try {
const url = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent(query)}&format=json&language=id&categories=${category}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
const { controller, clear } = createAbortControllerWithTimeout(TIMEOUT_MS);
const response = await fetch(url, {
signal: controller.signal,
headers: {
Accept: "application/json",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
},
});
clearTimeout(timeoutId);
if (!response.ok) {
log.warn({ status: response.status, query }, "SearXNG search failed");
return [];
}
const data = (await response.json()) as {
results?: Array<{ title?: string; url?: string; content?: string }>;
};
const results = data.results ?? [];
const mapped = results.slice(0, MAX_RESULTS).map((r) => ({
title: r.title ?? "",
url: r.url ?? "",
snippet: (r.content ?? "").slice(0, 500),
}));
// Store in cache (fire and forget — don't block on write)
if (redis) {
redis.setex(cacheKey, CACHE_TTL, JSON.stringify(mapped)).catch(() => {
// Cache write failed silently
try {
const response = await fetch(url, {
signal: controller.signal,
headers: {
Accept: "application/json",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
},
});
}
log.debug({ query, category, resultCount: mapped.length }, "SearXNG search OK");
return mapped;
if (!response.ok) {
log.warn({ status: response.status, query }, "SearXNG search failed");
return [];
}
const data = (await response.json()) as {
results?: Array<{ title?: string; url?: string; content?: string }>;
};
const results = data.results ?? [];
const mapped = results.slice(0, MAX_RESULTS).map((r) => ({
title: r.title ?? "",
url: r.url ?? "",
snippet: (r.content ?? "").slice(0, 500),
}));
// Store in cache (fire and forget — don't block on write)
if (redis) {
redis.setex(cacheKey, CACHE_TTL, JSON.stringify(mapped)).catch(() => {
// Cache write failed silently
});
}
log.debug({ query, category, resultCount: mapped.length }, "SearXNG search OK");
return mapped;
} finally {
clear();
}
} catch (err) {
log.warn(
{ error: err instanceof Error ? err.message : String(err), query },
@@ -1,6 +1,7 @@
import { resolve } from "node:dns/promises";
import { isIP } from "node:net";
import { createChildLogger } from "@bete/shared/logger";
import { createAbortControllerWithTimeout } from "@bete/shared/utils";
const log = createChildLogger("urlFetcher");
@@ -112,8 +113,7 @@ export async function fetchUrlSafely(
return { url, type: "error", error: "Unsafe URL blocked" };
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
const { controller, clear } = createAbortControllerWithTimeout(FETCH_TIMEOUT_MS);
try {
const response = await fetch(url, {
@@ -190,7 +190,7 @@ export async function fetchUrlSafely(
error: err instanceof Error ? err.message : String(err),
};
} finally {
clearTimeout(timeoutId);
clear();
}
}