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 { 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("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" }; } } }