// Search Posts // Searches Bluesky posts with rich filtering, then displays engagement stats. // Requires a Bluesky auth token (the searchPosts endpoint is not public). // Demonstrates: searchPosts, getLikes, getRepostedBy, getQuotes, toBskyUrl. // // Setup: // export BSKY_AUTH_TOKEN="your-access-jwt-here" // // Usage: npx tsx examples/search-posts.ts [options] // Examples: // npx tsx examples/search-posts.ts "bluesky" // npx tsx examples/search-posts.ts "typescript" --sort=latest --lang=en --limit=5 // npx tsx examples/search-posts.ts "atproto" --author=jay.bsky.team // npx tsx examples/search-posts.ts "bluesky" --tag=tech --tag=social import { getBskyAuthToken, fetchSearchPosts, fetchGetLikes, fetchGetRepostedBy, fetchGetQuotes, extractPostViewInfo, formatTruncated, formatEngagement, } from "../lib/index.js"; import type { SearchPostsOptions } from "../lib/index.js"; // --- Config --- const args = process.argv.slice(2); const query = args.find((a) => !a.startsWith("--")); if (!query) { console.error("Usage: npx tsx examples/search-posts.ts [options]"); console.error(""); console.error("Options:"); console.error(" --sort=top|latest Sort order (default: top)"); console.error(" --lang= Language filter (e.g. en, ja)"); console.error(" --author= Filter by author"); console.error(" --limit= Number of results (default: 10)"); console.error(" --tag= Filter by tag (can repeat)"); console.error(" --since= Posts after this time"); console.error(" --until= Posts before this time"); console.error(" --engagement Fetch detailed engagement for each post"); console.error(""); console.error("Requires BSKY_AUTH_TOKEN env var (see README for details)."); process.exit(1); } if (!getBskyAuthToken()) { console.error("Missing BSKY_AUTH_TOKEN environment variable.\n"); console.error("The searchPosts endpoint requires authentication."); console.error("To get a token, create an App Password at:"); console.error(" https://bsky.app/settings/app-passwords\n"); console.error("Then create a session to get an access JWT:"); console.error(` curl -s -X POST https://bsky.social/xrpc/com.atproto.server.createSession \\`); console.error(` -H "Content-Type: application/json" \\`); console.error(` -d '{"identifier":"your.handle","password":"your-app-password"}' \\`); console.error(` | jq -r .accessJwt\n`); console.error("Then export it:"); console.error(" export BSKY_AUTH_TOKEN=\"eyJ...\"\n"); process.exit(1); } const getFlag = (name: string): string | undefined => { const arg = args.find((a) => a.startsWith(`--${name}=`)); return arg?.split("=").slice(1).join("="); }; const hasFlag = (name: string): boolean => args.includes(`--${name}`); const getTags = (): string[] => args .filter((a) => a.startsWith("--tag=")) .map((a) => a.split("=").slice(1).join("=")); const options: SearchPostsOptions = { sort: (getFlag("sort") as "top" | "latest") ?? undefined, lang: getFlag("lang"), author: getFlag("author"), limit: getFlag("limit") ? Number(getFlag("limit")) : 10, tag: getTags().length ? getTags() : undefined, since: getFlag("since"), until: getFlag("until"), }; const showEngagement = hasFlag("engagement"); // --- Helpers --- const divider = () => console.log("─".repeat(60)); // --- Main --- const main = async () => { console.log(`\nSearching: "${query}"\n`); try { const data = await fetchSearchPosts(query, options); const posts = data.posts ?? []; if (posts.length === 0) { console.log("No posts found.\n"); return; } console.log(`Found ${posts.length} posts:\n`); divider(); for (const post of posts) { const info = extractPostViewInfo(post); console.log(` ${info.displayName} (@${info.handle})`); console.log(` ${formatTruncated(info.text, 200)}`); console.log(` ${formatEngagement({ likeCount: post.likeCount, repostCount: post.repostCount, quoteCount: post.quoteCount, replyCount: post.replyCount })}`); console.log(` ${info.bskyUrl}`); if (showEngagement && post.uri) { try { const [likes, reposts, quotes] = await Promise.all([ fetchGetLikes(post.uri, { limit: 3 }), fetchGetRepostedBy(post.uri, { limit: 3 }), fetchGetQuotes(post.uri, { limit: 3 }), ]); const likeNames = (likes.likes ?? []) .map((l: any) => `@${l.actor.handle}`) .join(", "); const repostNames = (reposts.repostedBy ?? []) .map((r: any) => `@${r.handle}`) .join(", "); const quoteTexts = (quotes.posts ?? []) .map((q: any) => { const qt = (q.record as any)?.text ?? ""; return formatTruncated(qt, 50); }); if (likeNames) console.log(` Liked by: ${likeNames}`); if (repostNames) console.log(` Reposted by: ${repostNames}`); if (quoteTexts.length) { for (const qt of quoteTexts) { console.log(` Quoted: "${qt}"`); } } } catch (err: any) { console.log(` (engagement fetch failed: ${err.message})`); } } divider(); } if (data.cursor) { console.log(`\n (more results available, cursor: ${data.cursor})\n`); } } catch (err: any) { console.error(`Error: ${err.message}`); process.exit(1); } }; main();