UNPKG

n8n-nodes-bluesky

Version:

⚠️ PROOF OF CONCEPT - Bluesky nodes for n8n (unmaintained, use at own risk)

244 lines (230 loc) 6.53 kB
import type { INodeType, INodeTypeDescription, IExecuteFunctions, INodeExecutionData, } from 'n8n-workflow'; export class BlueskySearch implements INodeType { description: INodeTypeDescription = { displayName: 'Bluesky Search', name: 'blueskySearch', group: ['transform'], version: 1, description: 'Search Bluesky posts using the AT Protocol', defaults: { name: 'Bluesky Search' }, icon: 'file:bluesky.svg', credentials: [ { name: 'blueskyApi', required: true }, ], inputs: ['main'], outputs: ['main'], properties: [ { displayName: 'Query', name: 'query', type: 'string', default: '', placeholder: 'search terms', description: 'Search query (supports Lucene syntax)', required: true, }, { displayName: 'Limit', name: 'limit', type: 'number', typeOptions: { minValue: 1, maxValue: 100 }, default: 25, description: 'Maximum number of results to return', }, { displayName: 'Sort', name: 'sort', type: 'options', options: [ { name: 'Latest', value: 'latest' }, { name: 'Top', value: 'top' }, ], default: 'latest', description: 'Sort order for results', }, { displayName: 'Additional Filters', name: 'additionalFilters', type: 'collection', placeholder: 'Add Filter', default: {}, options: [ { displayName: 'Author', name: 'author', type: 'string', default: '', placeholder: 'handle.bsky.social', description: 'Filter to posts by specific author', }, { displayName: 'Domain', name: 'domain', type: 'string', default: '', placeholder: 'example.com', description: 'Filter to posts linking to domain', }, { displayName: 'Language', name: 'lang', type: 'string', default: '', placeholder: 'en', description: 'Filter by language code', }, { displayName: 'Since', name: 'since', type: 'string', default: '', placeholder: '2025-01-01T00:00:00Z', description: 'Posts after this datetime (ISO 8601)', }, { displayName: 'Until', name: 'until', type: 'string', default: '', placeholder: '2025-12-31T23:59:59Z', description: 'Posts before this datetime (ISO 8601)', }, { displayName: 'Mentions', name: 'mentions', type: 'string', default: '', placeholder: 'handle.bsky.social', description: 'Filter by mentioned account', }, { displayName: 'Tags', name: 'tag', type: 'string', default: '', placeholder: 'hashtag1,hashtag2', description: 'Comma-separated hashtags (without #)', }, ], }, { displayName: 'Return Format', name: 'returnFormat', type: 'options', options: [ { name: 'Individual Posts', value: 'items' }, { name: 'Summary', value: 'summary' }, ], default: 'items', description: 'How to format the output', }, ], }; async execute(this: IExecuteFunctions) { const returnData: INodeExecutionData[] = []; // Get credentials const creds = await this.getCredentials('blueskyApi'); const identifier = String(creds.identifier); const password = String(creds.password); const host = (creds.host as string) || 'https://bsky.social'; // Get node parameters const query = this.getNodeParameter('query', 0) as string; const limit = this.getNodeParameter('limit', 0) as number; const sort = this.getNodeParameter('sort', 0) as string; const additionalFilters = this.getNodeParameter('additionalFilters', 0) as any; const returnFormat = this.getNodeParameter('returnFormat', 0) as string; if (!query.trim()) { throw new Error('Search query is required.'); } // 1) Create session (Bearer token) const session = await this.helpers.httpRequest({ method: 'POST', url: `${host}/xrpc/com.atproto.server.createSession`, body: { identifier, password }, json: true, }); const token = session?.accessJwt as string; if (!token) throw new Error('Failed to obtain Bluesky access token'); // 2) Build query params for searchPosts const queryParams: any = { q: query, limit, sort, }; // Add optional filters if (additionalFilters.author) queryParams.author = additionalFilters.author; if (additionalFilters.domain) queryParams.domain = additionalFilters.domain; if (additionalFilters.lang) queryParams.lang = additionalFilters.lang; if (additionalFilters.since) queryParams.since = additionalFilters.since; if (additionalFilters.until) queryParams.until = additionalFilters.until; if (additionalFilters.mentions) queryParams.mentions = additionalFilters.mentions; if (additionalFilters.tag) { // Convert comma-separated tags to array queryParams.tag = additionalFilters.tag.split(',').map((t: string) => t.trim()); } // 3) Call searchPosts const res = await this.helpers.httpRequest({ method: 'GET', url: `${host}/xrpc/app.bsky.feed.searchPosts`, qs: queryParams, headers: { Authorization: `Bearer ${token}` }, json: true, }); const posts = (res?.posts as any[]) || []; const hitsTotal = res?.hitsTotal || posts.length; const cursor = res?.cursor || null; // 4) Format output if (returnFormat === 'summary') { // Return summary returnData.push({ json: { query, hits_total: hitsTotal, returned: posts.length, cursor, posts: posts.map((p: any) => ({ uri: p.uri, author: p.author?.handle || 'unknown', text: p.record?.text || '', indexedAt: p.indexedAt, createdAt: p.record?.createdAt || '', })), }, }); } else { // Return individual posts as items for (const p of posts) { const postId = p.uri.split('/').pop(); const handle = p.author?.handle || 'unknown'; const webLink = `https://bsky.app/profile/${handle}/post/${postId}`; returnData.push({ json: { uri: p.uri, cid: p.cid, author: { did: p.author?.did, handle: p.author?.handle, displayName: p.author?.displayName, avatar: p.author?.avatar, }, text: p.record?.text || '', createdAt: p.record?.createdAt, indexedAt: p.indexedAt, replyCount: p.replyCount || 0, repostCount: p.repostCount || 0, likeCount: p.likeCount || 0, quoteCount: p.quoteCount || 0, webLink, rawPost: p, }, }); } } return [returnData]; } }