76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
|
|
import { keywordSearchViaThreadsApi } from "@/lib/threads-api";
|
||
|
|
import type { ThreadsApiCredentials } from "@/lib/threads-api/types";
|
||
|
|
import { cacheGet, cacheSet } from "../cache";
|
||
|
|
import { getSearchConfig } from "../config";
|
||
|
|
import { logSearchEvent } from "../logger";
|
||
|
|
import type { SearchProvider, SearchRequest, SearchResponse, SearchResult } from "../types";
|
||
|
|
|
||
|
|
export class ThreadsApiSearchProvider implements SearchProvider {
|
||
|
|
constructor(private credentials: ThreadsApiCredentials | null) {}
|
||
|
|
|
||
|
|
name() {
|
||
|
|
return "threads" as const;
|
||
|
|
}
|
||
|
|
|
||
|
|
enabled(): boolean {
|
||
|
|
return getSearchConfig().threads.enabled && !!this.credentials?.accessToken;
|
||
|
|
}
|
||
|
|
|
||
|
|
async search(req: SearchRequest): Promise<SearchResponse> {
|
||
|
|
if (!this.enabled() || !this.credentials) {
|
||
|
|
logSearchEvent({
|
||
|
|
kind: "provider",
|
||
|
|
provider: "threads",
|
||
|
|
status: "unavailable",
|
||
|
|
keyword: req.keyword,
|
||
|
|
});
|
||
|
|
return { provider: "threads", results: [], status: "unavailable" };
|
||
|
|
}
|
||
|
|
|
||
|
|
const cfg = getSearchConfig();
|
||
|
|
const cacheKey = `search:threads:${req.keyword}:${req.limit}`;
|
||
|
|
const cached = cacheGet<SearchResult[]>("threads", cacheKey);
|
||
|
|
if (cached) {
|
||
|
|
return { provider: "threads", results: cached, status: "success", skipReason: "cache_hit" };
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const posts = await keywordSearchViaThreadsApi(this.credentials, {
|
||
|
|
query: req.keyword,
|
||
|
|
limit: req.limit,
|
||
|
|
searchType: "RECENT",
|
||
|
|
});
|
||
|
|
|
||
|
|
const results: SearchResult[] = posts.map((p) => ({
|
||
|
|
title: p.text.slice(0, 120),
|
||
|
|
url: p.permalink ?? "",
|
||
|
|
snippet: p.text,
|
||
|
|
author: p.authorName ?? "",
|
||
|
|
publishedAt: p.postedAt,
|
||
|
|
source: "threads" as const,
|
||
|
|
threadId: p.externalId,
|
||
|
|
raw: { likeCount: p.likeCount, replyCount: p.replyCount },
|
||
|
|
}));
|
||
|
|
|
||
|
|
cacheSet("threads", cacheKey, results, cfg.threadsCacheTtlMs);
|
||
|
|
|
||
|
|
logSearchEvent({
|
||
|
|
kind: "provider",
|
||
|
|
provider: "threads",
|
||
|
|
status: "success",
|
||
|
|
keyword: req.keyword,
|
||
|
|
count: results.length,
|
||
|
|
});
|
||
|
|
|
||
|
|
return { provider: "threads", results, status: "success" };
|
||
|
|
} catch {
|
||
|
|
logSearchEvent({
|
||
|
|
kind: "provider",
|
||
|
|
provider: "threads",
|
||
|
|
status: "unavailable",
|
||
|
|
keyword: req.keyword,
|
||
|
|
});
|
||
|
|
return { provider: "threads", results: [], status: "unavailable" };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|