Spaces:
Running
Running
import axios from "axios"; | |
import type { PreviewServer, ViteDevServer } from "vite"; | |
import { fetchSearXNG } from "./fetchSearXNG"; | |
import { rankSearchResults } from "./rankSearchResults"; | |
import { | |
incrementGraphicalSearchesSinceLastRestart, | |
incrementTextualSearchesSinceLastRestart, | |
} from "./searchesSinceLastRestart"; | |
import { verifyTokenAndRateLimit } from "./verifyTokenAndRateLimit"; | |
type TextResult = [title: string, content: string, url: string]; | |
type ImageResult = [ | |
title: string, | |
url: string, | |
thumbnailSource: string, | |
sourceUrl: string, | |
]; | |
export function searchEndpointServerHook< | |
T extends ViteDevServer | PreviewServer, | |
>(server: T) { | |
server.middlewares.use(async (request, response, next) => { | |
if (!request.url?.startsWith("/search/")) return next(); | |
const url = new URL(request.url, `http://${request.headers.host}`); | |
const query = url.searchParams.get("q"); | |
const token = url.searchParams.get("token"); | |
const limit = Number(url.searchParams.get("limit")) || 30; | |
if (!query) { | |
response.statusCode = 400; | |
response.end(JSON.stringify({ error: "Missing query parameter" })); | |
return; | |
} | |
const { isAuthorized, statusCode, error } = | |
await verifyTokenAndRateLimit(token); | |
if (!isAuthorized && statusCode && error) { | |
response.statusCode = statusCode; | |
response.end(JSON.stringify({ error })); | |
return; | |
} | |
try { | |
const isTextSearch = request.url?.startsWith("/search/text"); | |
const searchType = isTextSearch ? "text" : "images"; | |
const searxngResults = await fetchSearXNG(query, searchType, limit); | |
if (isTextSearch) { | |
const results = searxngResults as TextResult[]; | |
const rankedResults = await rankSearchResults(query, results); | |
incrementTextualSearchesSinceLastRestart(); | |
response.setHeader("Content-Type", "application/json"); | |
response.end(JSON.stringify(rankedResults)); | |
} else { | |
const results = searxngResults as ImageResult[]; | |
const rankedResults = await rankSearchResults( | |
query, | |
results.map( | |
([title, url, , sourceUrl]) => | |
[ | |
title.slice(0, 100), | |
sourceUrl.slice(0, 100), | |
url.slice(0, 100), | |
] as TextResult, | |
), | |
); | |
const processedResults = ( | |
await Promise.all( | |
results | |
.filter((_, index) => | |
rankedResults.some(([title]) => title === results[index][0]), | |
) | |
.map(async ([title, url, thumbnailSource, sourceUrl]) => { | |
try { | |
const axiosResponse = await axios.get(thumbnailSource, { | |
responseType: "arraybuffer", | |
}); | |
const contentType = axiosResponse.headers["content-type"]; | |
const base64 = Buffer.from(axiosResponse.data).toString( | |
"base64", | |
); | |
return [ | |
title, | |
url, | |
`data:${contentType};base64,${base64}`, | |
sourceUrl, | |
] as ImageResult; | |
} catch { | |
return null; | |
} | |
}), | |
) | |
).filter((result): result is ImageResult => result !== null); | |
incrementGraphicalSearchesSinceLastRestart(); | |
response.setHeader("Content-Type", "application/json"); | |
response.end(JSON.stringify(processedResults)); | |
} | |
} catch (error) { | |
console.error( | |
"Error processing search:", | |
error instanceof Error ? error.message : error, | |
); | |
response.statusCode = 500; | |
response.end(JSON.stringify({ error: "Internal server error" })); | |
} | |
}); | |
} | |