n8n-nodes-atproto 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,6 +97,22 @@ Worked examples of common workflows. Each one is a chain of nodes — names in *
97
97
 
98
98
  **Convenience fields:** the upload also exposes `cid` / `mimeType` / `size` at the top level, so `{{ $json.cid }}` works without drilling into `blob.ref.$link`.
99
99
 
100
+ ### Post a Bluesky link with an embed card
101
+
102
+ The **Bluesky** node's Post → Create operation builds an `app.bsky.embed.external` link card for you. Set **External Link URL** under Options; with **Auto-Scrape Link Metadata** on (the default) it fetches the page's OpenGraph title, description and thumbnail.
103
+
104
+ ```
105
+ [Bluesky: Post → Create]
106
+ Text: Great read 👇
107
+ Options:
108
+ External Link URL: https://example.com/article
109
+ Auto-Scrape Link Metadata: ✅ ← fetches og:title / og:description / og:image
110
+ ```
111
+
112
+ - **Overrides:** set **Link Title** / **Link Description** to replace the scraped text, or **Link Thumbnail Binary Property** to use your own image instead of the scraped one.
113
+ - **Scrape failures are fatal:** if auto-scrape is on and the page can't be fetched, the item errors — enable *Continue On Fail* to tolerate it. The thumbnail itself is best-effort: the card still posts if the image is missing or exceeds Bluesky's 1 MB blob limit.
114
+ - A post carries **either** an image embed **or** a link card, not both.
115
+
100
116
  ### Round-trip a blob (download, transform, re-upload)
101
117
 
102
118
  Mirror a blob from another user's repo into your own. Useful for archival, format conversion, or re-hosting.
@@ -62,6 +62,7 @@ async function createPost(agent, params) {
62
62
  langs: params.langs && params.langs.length > 0 ? params.langs : ["en"],
63
63
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
64
64
  };
65
+ if (params.image && params.external) throw new Error("A post can carry either an image embed or an external link card, not both.");
65
66
  if (params.image) record.embed = {
66
67
  $type: "app.bsky.embed.images",
67
68
  images: [{
@@ -69,6 +70,18 @@ async function createPost(agent, params) {
69
70
  image: params.image.blob
70
71
  }]
71
72
  };
73
+ else if (params.external) {
74
+ const { uri, title, description, thumb } = params.external;
75
+ record.embed = {
76
+ $type: "app.bsky.embed.external",
77
+ external: {
78
+ uri,
79
+ title,
80
+ description,
81
+ ...thumb ? { thumb } : {}
82
+ }
83
+ };
84
+ }
72
85
  const res = await agent.com.atproto.repo.createRecord({
73
86
  repo: agent.did,
74
87
  collection: "app.bsky.feed.post",
@@ -186,6 +199,113 @@ async function followUser(agent, handleOrDid) {
186
199
  };
187
200
  }
188
201
  //#endregion
202
+ //#region nodes/Bluesky/external.ts
203
+ var USER_AGENT = "n8n-nodes-atproto (link-card scraper)";
204
+ var defaultFetch = (url, init) => fetch(url, init);
205
+ var errMessage = (err) => err instanceof Error ? err.message : String(err);
206
+ /**
207
+ * Fetch a URL and extract its link-card metadata. Throws if the page cannot
208
+ * be retrieved (network error or non-2xx) — the caller treats a failed scrape
209
+ * as a hard failure of the item.
210
+ */
211
+ async function fetchExternalMetadata(url, doFetch = defaultFetch) {
212
+ let res;
213
+ let failure;
214
+ try {
215
+ res = await doFetch(url, { headers: {
216
+ "user-agent": USER_AGENT,
217
+ accept: "text/html,application/xhtml+xml"
218
+ } });
219
+ } catch (err) {
220
+ failure = errMessage(err);
221
+ }
222
+ if (failure !== void 0) throw new Error(`Could not fetch ${url} for link-card metadata: ${failure}`);
223
+ if (!res || !res.ok) {
224
+ const detail = res ? `HTTP ${res.status} ${res.statusText}` : "no response";
225
+ throw new Error(`Could not fetch ${url} for link-card metadata: ${detail}`);
226
+ }
227
+ return parseOpenGraph(await res.text(), url);
228
+ }
229
+ /**
230
+ * Download a thumbnail image. Returns the raw bytes and the response's
231
+ * content-type. Throws on a non-2xx response; the caller treats the thumbnail
232
+ * as best-effort and posts without it on failure.
233
+ */
234
+ async function fetchThumbnail(url, doFetch = defaultFetch) {
235
+ const res = await doFetch(url, { headers: { "user-agent": USER_AGENT } });
236
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
237
+ const bytes = new Uint8Array(await res.arrayBuffer());
238
+ const contentType = res.headers.get("content-type");
239
+ return {
240
+ bytes,
241
+ mimeType: contentType ? contentType.split(";")[0].trim() : "application/octet-stream"
242
+ };
243
+ }
244
+ var META_TAG = /<meta\b[^>]*>/gi;
245
+ var TITLE_TAG = /<title[^>]*>([\s\S]*?)<\/title>/i;
246
+ var NAMED_ENTITIES = {
247
+ amp: "&",
248
+ lt: "<",
249
+ gt: ">",
250
+ quot: "\"",
251
+ apos: "'",
252
+ nbsp: " "
253
+ };
254
+ function decodeEntities(input) {
255
+ return input.replace(/&(#x?[0-9a-f]+|[a-z][a-z0-9]*);/gi, (whole, body) => {
256
+ if (body[0] === "#") {
257
+ const code = body[1].toLowerCase() === "x" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
258
+ return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
259
+ }
260
+ return NAMED_ENTITIES[body.toLowerCase()] ?? whole;
261
+ });
262
+ }
263
+ /** Read an attribute value (double- or single-quoted) from a single tag. */
264
+ function getAttr(tag, name) {
265
+ const match = new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i").exec(tag);
266
+ if (!match) return void 0;
267
+ return match[2] ?? match[3];
268
+ }
269
+ /**
270
+ * Extract link-card metadata from page HTML.
271
+ *
272
+ * Precedence: `og:*` → `twitter:*` → `<title>` / `<meta name="description">`.
273
+ * A relative `og:image` is resolved against `baseUrl`. Returns empty strings
274
+ * for missing title/description (both are required by the lexicon).
275
+ */
276
+ function parseOpenGraph(html, baseUrl) {
277
+ const props = /* @__PURE__ */ new Map();
278
+ for (const tag of html.match(META_TAG) ?? []) {
279
+ const key = getAttr(tag, "property") ?? getAttr(tag, "name");
280
+ const content = getAttr(tag, "content");
281
+ if (key && content !== void 0 && !props.has(key.toLowerCase())) props.set(key.toLowerCase(), content);
282
+ }
283
+ const pick = (...keys) => {
284
+ for (const key of keys) {
285
+ const value = props.get(key);
286
+ if (value !== void 0 && value !== "") return value;
287
+ }
288
+ };
289
+ let title = pick("og:title", "twitter:title");
290
+ if (title === void 0) {
291
+ const match = TITLE_TAG.exec(html);
292
+ if (match) title = match[1].trim();
293
+ }
294
+ const description = pick("og:description", "twitter:description", "description") ?? "";
295
+ const imageRaw = pick("og:image", "og:image:url", "twitter:image");
296
+ let image;
297
+ if (imageRaw) try {
298
+ image = new URL(decodeEntities(imageRaw), baseUrl).toString();
299
+ } catch {
300
+ image = void 0;
301
+ }
302
+ return {
303
+ title: decodeEntities(title ?? ""),
304
+ description: decodeEntities(description),
305
+ image
306
+ };
307
+ }
308
+ //#endregion
189
309
  //#region nodes/Bluesky/AtprotoBluesky.node.ts
190
310
  var AtprotoBluesky = class {
191
311
  description = {
@@ -354,6 +474,40 @@ var AtprotoBluesky = class {
354
474
  ]
355
475
  } },
356
476
  options: [
477
+ {
478
+ displayName: "Auto-Scrape Link Metadata",
479
+ name: "externalAutoScrape",
480
+ type: "boolean",
481
+ default: true,
482
+ description: "Whether to fetch the page OpenGraph title, description and thumbnail. The item fails if the page cannot be fetched.",
483
+ displayOptions: { show: { "/operation": ["create"] } }
484
+ },
485
+ {
486
+ displayName: "External Link URL",
487
+ name: "externalUrl",
488
+ type: "string",
489
+ default: "",
490
+ placeholder: "https://example.com/article",
491
+ description: "URL to embed as a link card below the post. Leave empty for no card.",
492
+ displayOptions: { show: { "/operation": ["create"] } }
493
+ },
494
+ {
495
+ displayName: "Image Alt Text",
496
+ name: "imageAlt",
497
+ type: "string",
498
+ default: "",
499
+ description: "Accessibility description of the embedded image",
500
+ displayOptions: { show: { "/operation": ["create"] } }
501
+ },
502
+ {
503
+ displayName: "Image Binary Property",
504
+ name: "imageBinaryProperty",
505
+ type: "string",
506
+ default: "",
507
+ placeholder: "data",
508
+ description: "Name of the binary property on the incoming item containing an image to embed. Leave empty for a text-only post.",
509
+ displayOptions: { show: { "/operation": ["create"] } }
510
+ },
357
511
  {
358
512
  displayName: "Languages",
359
513
  name: "langs",
@@ -363,20 +517,28 @@ var AtprotoBluesky = class {
363
517
  description: "Comma-separated BCP-47 language tags for the post. Defaults to en."
364
518
  },
365
519
  {
366
- displayName: "Image Binary Property",
367
- name: "imageBinaryProperty",
520
+ displayName: "Link Description",
521
+ name: "externalDescription",
522
+ type: "string",
523
+ default: "",
524
+ description: "Card description. Overrides the scraped description when set.",
525
+ displayOptions: { show: { "/operation": ["create"] } }
526
+ },
527
+ {
528
+ displayName: "Link Thumbnail Binary Property",
529
+ name: "externalThumbBinaryProperty",
368
530
  type: "string",
369
531
  default: "",
370
532
  placeholder: "data",
371
- description: "Name of the binary property on the incoming item containing an image to embed. Leave empty for a text-only post.",
533
+ description: "Name of a binary property holding a custom card thumbnail. Overrides the scraped image.",
372
534
  displayOptions: { show: { "/operation": ["create"] } }
373
535
  },
374
536
  {
375
- displayName: "Image Alt Text",
376
- name: "imageAlt",
537
+ displayName: "Link Title",
538
+ name: "externalTitle",
377
539
  type: "string",
378
540
  default: "",
379
- description: "Accessibility description of the embedded image",
541
+ description: "Card title. Overrides the scraped title when set.",
380
542
  displayOptions: { show: { "/operation": ["create"] } }
381
543
  }
382
544
  ]
@@ -395,6 +557,7 @@ var AtprotoBluesky = class {
395
557
  const text = this.getNodeParameter("text", i);
396
558
  const options = this.getNodeParameter("options", i, {});
397
559
  const langs = parseLangs(options.langs);
560
+ if (options.imageBinaryProperty && options.externalUrl) throw new n8n_workflow.NodeOperationError(this.getNode(), "A post can carry either an image embed or an external link card, not both.", { itemIndex: i });
398
561
  let image;
399
562
  if (options.imageBinaryProperty) {
400
563
  const binaryData = await this.helpers.getBinaryDataBuffer(i, options.imageBinaryProperty);
@@ -404,10 +567,38 @@ var AtprotoBluesky = class {
404
567
  alt: options.imageAlt ?? ""
405
568
  };
406
569
  }
570
+ let external;
571
+ if (options.externalUrl) {
572
+ let title = options.externalTitle ?? "";
573
+ let description = options.externalDescription ?? "";
574
+ let imageUrl;
575
+ if (options.externalAutoScrape !== false) {
576
+ const meta = await fetchExternalMetadata(options.externalUrl);
577
+ title = options.externalTitle || meta.title;
578
+ description = options.externalDescription || meta.description;
579
+ imageUrl = meta.image;
580
+ }
581
+ let thumb;
582
+ if (options.externalThumbBinaryProperty) {
583
+ const thumbData = await this.helpers.getBinaryDataBuffer(i, options.externalThumbBinaryProperty);
584
+ const mimeType = items[i].binary?.[options.externalThumbBinaryProperty]?.mimeType ?? "application/octet-stream";
585
+ thumb = (await agent.com.atproto.repo.uploadBlob(thumbData, { encoding: mimeType })).data.blob;
586
+ } else if (imageUrl) try {
587
+ const { bytes, mimeType } = await fetchThumbnail(imageUrl);
588
+ if (bytes.byteLength <= 1e6) thumb = (await agent.com.atproto.repo.uploadBlob(bytes, { encoding: mimeType })).data.blob;
589
+ } catch {}
590
+ external = {
591
+ uri: options.externalUrl,
592
+ title,
593
+ description,
594
+ ...thumb ? { thumb } : {}
595
+ };
596
+ }
407
597
  result = await createPost(agent, {
408
598
  text,
409
599
  langs,
410
- image
600
+ image,
601
+ external
411
602
  });
412
603
  } else if (resource === "post" && operation === "reply") result = await replyToPost(agent, {
413
604
  text: this.getNodeParameter("text", i),
@@ -1 +1 @@
1
- {"version":3,"file":"AtprotoBluesky.node.js","names":[],"sources":["../../../nodes/Bluesky/postUri.ts","../../../nodes/Bluesky/operations.ts","../../../nodes/Bluesky/AtprotoBluesky.node.ts"],"sourcesContent":["/**\n * Parse a Bluesky post reference into its repo/collection/rkey parts.\n *\n * Accepts either an AT-URI or an HTTPS bsky.app URL:\n * at://did:plc:abc.../app.bsky.feed.post/3jzfcijpj2z2a\n * https://bsky.app/profile/handle.bsky.social/post/3jzfcijpj2z2a\n *\n * Returns `{ repo, collection, rkey }` where `repo` is a DID or handle.\n * For the HTTPS form, `repo` is the handle from the URL — call\n * `agent.com.atproto.identity.resolveHandle` if you need a DID.\n */\n\nimport type { Agent } from '@atproto/api';\n\nexport interface PostRef {\n repo: string;\n collection: string;\n rkey: string;\n}\n\nconst AT_URI_RE = /^at:\\/\\/([^/]+)\\/([^/]+)\\/([^/]+)$/;\nconst BSKY_URL_RE =\n /^https?:\\/\\/bsky\\.app\\/profile\\/([^/]+)\\/post\\/([^/?#]+)/;\n\nexport function parsePostUri(input: string): PostRef {\n const trimmed = input.trim();\n\n const atMatch = trimmed.match(AT_URI_RE);\n if (atMatch) {\n return { repo: atMatch[1], collection: atMatch[2], rkey: atMatch[3] };\n }\n\n const urlMatch = trimmed.match(BSKY_URL_RE);\n if (urlMatch) {\n return {\n repo: urlMatch[1],\n collection: 'app.bsky.feed.post',\n rkey: urlMatch[2],\n };\n }\n\n throw new Error(\n `Could not parse post reference '${input}'. Expected an at:// URI or a https://bsky.app/profile/.../post/... URL.`,\n );\n}\n\n/**\n * Resolve a `PostRef` to its actual `{ uri, cid }` by calling getRecord.\n * Handles the HTTPS-URL case where `repo` is a handle (resolves to DID first).\n */\nexport async function fetchPostRef(\n agent: Agent,\n ref: PostRef,\n): Promise<{ uri: string; cid: string; value: Record<string, unknown> }> {\n let repo = ref.repo;\n if (!repo.startsWith('did:')) {\n const resolved = await agent.com.atproto.identity.resolveHandle({\n handle: repo,\n });\n repo = resolved.data.did;\n }\n\n const res = await agent.com.atproto.repo.getRecord({\n repo,\n collection: ref.collection,\n rkey: ref.rkey,\n });\n\n return {\n uri: res.data.uri,\n cid: res.data.cid ?? '',\n value: res.data.value as Record<string, unknown>,\n };\n}\n","/**\n * Bluesky-specific operations. Thin wrappers around the AT Protocol\n * createRecord XRPC that handle the per-record-type ceremony:\n *\n * - Post.create → builds the rich-text record (auto-facet detection),\n * optional language tags, optional image embed\n * - Post.reply → walks parent → root and builds the reply ref\n * - Post.quote → builds an `app.bsky.embed.record` embed\n * - Like.create → fetches subject CID, creates `app.bsky.feed.like`\n * - Repost.create → same shape as Like, in `app.bsky.feed.repost`\n * - Follow.create → resolves handle → DID, creates `app.bsky.graph.follow`\n *\n * All operations return `{ uri, cid }` from the underlying createRecord\n * call so the workflow can chain on the new record.\n */\n\nimport type { Agent } from '@atproto/api';\nimport { RichText } from '@atproto/api';\n\nimport { fetchPostRef, parsePostUri } from './postUri';\n\n// ---------------------------------------------------------------------------\n// Common types\n// ---------------------------------------------------------------------------\n\ntype StrongRef = { uri: string; cid: string };\n\nexport interface CreateResult {\n uri: string;\n cid: string;\n}\n\n// ---------------------------------------------------------------------------\n// Rich-text builder — used by Create / Reply / Quote\n// ---------------------------------------------------------------------------\n\n/**\n * Build the rich-text payload for a post: `{ text, facets }`.\n *\n * Uses `RichText.detectFacets(agent)` from @atproto/api, which finds\n * @mentions (and resolves them to DIDs via the PDS), URLs and #tags,\n * with correct byte offsets per the lexicon.\n */\nasync function buildRichText(\n agent: Agent,\n text: string,\n): Promise<{ text: string; facets?: unknown[] }> {\n const rt = new RichText({ text });\n await rt.detectFacets(agent);\n return {\n text: rt.text,\n ...(rt.facets && rt.facets.length > 0 ? { facets: rt.facets } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Post — Create\n// ---------------------------------------------------------------------------\n\nexport interface CreatePostParams {\n text: string;\n langs?: string[];\n /** Optional image embed: pre-uploaded blob + alt text */\n image?: { blob: unknown; alt: string };\n}\n\nexport async function createPost(\n agent: Agent,\n params: CreatePostParams,\n): Promise<CreateResult> {\n const rt = await buildRichText(agent, params.text);\n\n const record: Record<string, unknown> = {\n $type: 'app.bsky.feed.post',\n ...rt,\n langs: params.langs && params.langs.length > 0 ? params.langs : ['en'],\n createdAt: new Date().toISOString(),\n };\n\n if (params.image) {\n record.embed = {\n $type: 'app.bsky.embed.images',\n images: [{ alt: params.image.alt, image: params.image.blob }],\n };\n }\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection: 'app.bsky.feed.post',\n record,\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n\n// ---------------------------------------------------------------------------\n// Post — Reply\n// ---------------------------------------------------------------------------\n\nexport interface ReplyParams {\n parentUri: string;\n text: string;\n langs?: string[];\n}\n\n/**\n * Reply to a post. Walks the parent record to discover the thread root —\n * if the parent is itself a reply, we reuse its `reply.root`; otherwise\n * the parent IS the root.\n */\nexport async function replyToPost(\n agent: Agent,\n params: ReplyParams,\n): Promise<CreateResult> {\n const parentRef = parsePostUri(params.parentUri);\n const parent = await fetchPostRef(agent, parentRef);\n\n const parentStrongRef: StrongRef = { uri: parent.uri, cid: parent.cid };\n const parentReply = (parent.value as { reply?: { root?: StrongRef } }).reply;\n const root: StrongRef = parentReply?.root ?? parentStrongRef;\n\n const rt = await buildRichText(agent, params.text);\n\n const record = {\n $type: 'app.bsky.feed.post',\n ...rt,\n langs:\n params.langs && params.langs.length > 0 ? params.langs : ['en'],\n reply: { root, parent: parentStrongRef },\n createdAt: new Date().toISOString(),\n };\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection: 'app.bsky.feed.post',\n record,\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n\n// ---------------------------------------------------------------------------\n// Post — Quote\n// ---------------------------------------------------------------------------\n\nexport interface QuoteParams {\n quotedUri: string;\n text: string;\n langs?: string[];\n}\n\nexport async function quotePost(\n agent: Agent,\n params: QuoteParams,\n): Promise<CreateResult> {\n const ref = parsePostUri(params.quotedUri);\n const subject = await fetchPostRef(agent, ref);\n\n const rt = await buildRichText(agent, params.text);\n\n const record = {\n $type: 'app.bsky.feed.post',\n ...rt,\n langs:\n params.langs && params.langs.length > 0 ? params.langs : ['en'],\n embed: {\n $type: 'app.bsky.embed.record',\n record: { uri: subject.uri, cid: subject.cid },\n },\n createdAt: new Date().toISOString(),\n };\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection: 'app.bsky.feed.post',\n record,\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n\n// ---------------------------------------------------------------------------\n// Engagement — Like / Repost\n// ---------------------------------------------------------------------------\n\nasync function createSubjectRef(\n agent: Agent,\n postUri: string,\n collection: 'app.bsky.feed.like' | 'app.bsky.feed.repost',\n): Promise<CreateResult> {\n const ref = parsePostUri(postUri);\n const subject = await fetchPostRef(agent, ref);\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection,\n record: {\n $type: collection,\n subject: { uri: subject.uri, cid: subject.cid },\n createdAt: new Date().toISOString(),\n },\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n\nexport function likePost(\n agent: Agent,\n postUri: string,\n): Promise<CreateResult> {\n return createSubjectRef(agent, postUri, 'app.bsky.feed.like');\n}\n\nexport function repostPost(\n agent: Agent,\n postUri: string,\n): Promise<CreateResult> {\n return createSubjectRef(agent, postUri, 'app.bsky.feed.repost');\n}\n\n// ---------------------------------------------------------------------------\n// Graph — Follow\n// ---------------------------------------------------------------------------\n\n/**\n * Follow a user. Accepts either a handle (`alice.bsky.social`) or a DID.\n * Handles are resolved via `com.atproto.identity.resolveHandle` first.\n */\nexport async function followUser(\n agent: Agent,\n handleOrDid: string,\n): Promise<CreateResult> {\n const trimmed = handleOrDid.trim();\n let did = trimmed;\n if (!did.startsWith('did:')) {\n // Strip leading @ if the user typed @alice.bsky.social\n const handle = did.startsWith('@') ? did.slice(1) : did;\n const resolved = await agent.com.atproto.identity.resolveHandle({\n handle,\n });\n did = resolved.data.did;\n }\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection: 'app.bsky.graph.follow',\n record: {\n $type: 'app.bsky.graph.follow',\n subject: did,\n createdAt: new Date().toISOString(),\n },\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n","/**\n * Bluesky node — opinionated, task-shaped wrappers around the\n * AT Protocol `app.bsky.*` lexicons. Hides NSIDs and lexicon details\n * for the most common Bluesky tasks: posting, replying, quoting,\n * liking, reposting and following.\n *\n * Power users should keep using the generic AT Protocol node for\n * anything not covered here, or for non-default collection NSIDs.\n */\n\nimport type {\n IDataObject,\n IExecuteFunctions,\n INodeExecutionData,\n INodeType,\n INodeTypeDescription,\n} from 'n8n-workflow';\nimport { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';\n\nimport { createAgent } from '../Atproto/shared';\nimport {\n createPost,\n followUser,\n likePost,\n quotePost,\n replyToPost,\n repostPost,\n} from './operations';\n\nexport class AtprotoBluesky implements INodeType {\n description: INodeTypeDescription = {\n displayName: 'Bluesky',\n name: 'atprotoBluesky',\n icon: 'file:bluesky.svg',\n group: ['transform'],\n version: 1,\n subtitle:\n '={{ $parameter[\"operation\"] + \": \" + $parameter[\"resource\"] }}',\n description: 'Post, reply, quote, like, repost and follow on Bluesky',\n defaults: {\n name: 'Bluesky',\n },\n usableAsTool: true,\n inputs: [NodeConnectionTypes.Main],\n outputs: [NodeConnectionTypes.Main],\n credentials: [\n {\n name: 'atprotoApi',\n required: true,\n },\n ],\n properties: [\n // ------------------------------------------------------------------\n // Resource\n // ------------------------------------------------------------------\n {\n displayName: 'Resource',\n name: 'resource',\n type: 'options',\n noDataExpression: true,\n // Sorted alphabetically per @n8n/community-nodes lint rule.\n options: [\n { name: 'Follow', value: 'follow' },\n { name: 'Like', value: 'like' },\n { name: 'Post', value: 'post' },\n { name: 'Repost', value: 'repost' },\n ],\n default: 'post',\n },\n\n // ------------------------------------------------------------------\n // Operation — Post\n // ------------------------------------------------------------------\n {\n displayName: 'Operation',\n name: 'operation',\n type: 'options',\n noDataExpression: true,\n displayOptions: {\n show: { resource: ['post'] },\n },\n // Sorted alphabetically per @n8n/community-nodes lint rule.\n options: [\n {\n name: 'Create',\n value: 'create',\n description: 'Create a new post',\n action: 'Create a post',\n },\n {\n name: 'Quote',\n value: 'quote',\n description: 'Quote-post an existing post',\n action: 'Quote a post',\n },\n {\n name: 'Reply',\n value: 'reply',\n description: 'Reply to an existing post',\n action: 'Reply to a post',\n },\n ],\n default: 'create',\n },\n\n // ------------------------------------------------------------------\n // Operation — Like / Repost / Follow (single operation, hidden)\n // ------------------------------------------------------------------\n {\n displayName: 'Operation',\n name: 'operation',\n type: 'options',\n noDataExpression: true,\n displayOptions: {\n show: { resource: ['like', 'repost', 'follow'] },\n },\n options: [\n {\n name: 'Create',\n value: 'create',\n action: 'Create',\n },\n ],\n default: 'create',\n },\n\n // ------------------------------------------------------------------\n // Text — Post Create / Reply / Quote\n // ------------------------------------------------------------------\n {\n displayName: 'Text',\n name: 'text',\n type: 'string',\n required: true,\n default: '',\n placeholder: 'Hello @alice.bsky.social, check this out!',\n description:\n 'The post text. Mentions (@handle), URLs and #hashtags are auto-detected and rendered as rich-text facets.',\n typeOptions: {\n rows: 4,\n },\n displayOptions: {\n show: {\n resource: ['post'],\n operation: ['create', 'reply', 'quote'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // Parent URI — Reply\n // ------------------------------------------------------------------\n {\n displayName: 'Reply To',\n name: 'parentUri',\n type: 'string',\n required: true,\n default: '',\n placeholder:\n 'at://did:plc:.../app.bsky.feed.post/3jzfc... or https://bsky.app/profile/.../post/3jzfc...',\n description:\n 'The post to reply to. Accepts either an at:// URI or a bsky.app URL.',\n displayOptions: {\n show: {\n resource: ['post'],\n operation: ['reply'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // Quoted URI — Quote\n // ------------------------------------------------------------------\n {\n displayName: 'Quoted Post',\n name: 'quotedUri',\n type: 'string',\n required: true,\n default: '',\n placeholder:\n 'at://did:plc:.../app.bsky.feed.post/3jzfc... or https://bsky.app/profile/.../post/3jzfc...',\n description:\n 'The post to quote. Accepts either an at:// URI or a bsky.app URL.',\n displayOptions: {\n show: {\n resource: ['post'],\n operation: ['quote'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // Subject — Like / Repost\n // ------------------------------------------------------------------\n {\n displayName: 'Post',\n name: 'subjectUri',\n type: 'string',\n required: true,\n default: '',\n placeholder:\n 'at://did:plc:.../app.bsky.feed.post/3jzfc... or https://bsky.app/profile/.../post/3jzfc...',\n description: 'The post to like or repost. Accepts at:// URI or bsky.app URL.',\n displayOptions: {\n show: {\n resource: ['like', 'repost'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // User — Follow\n // ------------------------------------------------------------------\n {\n displayName: 'User',\n name: 'subjectUser',\n type: 'string',\n required: true,\n default: '',\n placeholder: 'alice.bsky.social or did:plc:...',\n description: 'The handle or DID of the user to follow',\n displayOptions: {\n show: {\n resource: ['follow'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // Options — Post Create / Reply / Quote\n // ------------------------------------------------------------------\n {\n displayName: 'Options',\n name: 'options',\n type: 'collection',\n placeholder: 'Add Option',\n default: {},\n displayOptions: {\n show: {\n resource: ['post'],\n operation: ['create', 'reply', 'quote'],\n },\n },\n options: [\n {\n displayName: 'Languages',\n name: 'langs',\n type: 'string',\n default: 'en',\n placeholder: 'en or en,is,fr',\n description:\n 'Comma-separated BCP-47 language tags for the post. Defaults to en.',\n },\n {\n displayName: 'Image Binary Property',\n name: 'imageBinaryProperty',\n type: 'string',\n default: '',\n placeholder: 'data',\n description:\n 'Name of the binary property on the incoming item containing an image to embed. Leave empty for a text-only post.',\n displayOptions: {\n show: {\n '/operation': ['create'],\n },\n },\n },\n {\n displayName: 'Image Alt Text',\n name: 'imageAlt',\n type: 'string',\n default: '',\n description: 'Accessibility description of the embedded image',\n displayOptions: {\n show: {\n '/operation': ['create'],\n },\n },\n },\n ],\n },\n ],\n };\n\n async execute(this: IExecuteFunctions) {\n const items = this.getInputData();\n const returnData: INodeExecutionData[] = [];\n\n const credentials = await this.getCredentials('atprotoApi');\n const agent = await createAgent(credentials as IDataObject);\n\n for (let i = 0; i < items.length; i++) {\n try {\n const resource = this.getNodeParameter('resource', i) as string;\n const operation = this.getNodeParameter('operation', i) as string;\n\n let result: IDataObject;\n\n if (resource === 'post' && operation === 'create') {\n const text = this.getNodeParameter('text', i) as string;\n const options = this.getNodeParameter(\n 'options',\n i,\n {},\n ) as {\n langs?: string;\n imageBinaryProperty?: string;\n imageAlt?: string;\n };\n\n const langs = parseLangs(options.langs);\n let image: { blob: unknown; alt: string } | undefined;\n\n if (options.imageBinaryProperty) {\n const binaryData = await this.helpers.getBinaryDataBuffer(\n i,\n options.imageBinaryProperty,\n );\n const mimeType =\n items[i].binary?.[options.imageBinaryProperty]?.mimeType ??\n 'application/octet-stream';\n const uploaded = await agent.com.atproto.repo.uploadBlob(\n binaryData,\n { encoding: mimeType },\n );\n image = {\n blob: uploaded.data.blob,\n alt: options.imageAlt ?? '',\n };\n }\n\n result = (await createPost(agent, {\n text,\n langs,\n image,\n })) as unknown as IDataObject;\n } else if (resource === 'post' && operation === 'reply') {\n const text = this.getNodeParameter('text', i) as string;\n const parentUri = this.getNodeParameter('parentUri', i) as string;\n const options = this.getNodeParameter('options', i, {}) as {\n langs?: string;\n };\n\n result = (await replyToPost(agent, {\n text,\n parentUri,\n langs: parseLangs(options.langs),\n })) as unknown as IDataObject;\n } else if (resource === 'post' && operation === 'quote') {\n const text = this.getNodeParameter('text', i) as string;\n const quotedUri = this.getNodeParameter('quotedUri', i) as string;\n const options = this.getNodeParameter('options', i, {}) as {\n langs?: string;\n };\n\n result = (await quotePost(agent, {\n text,\n quotedUri,\n langs: parseLangs(options.langs),\n })) as unknown as IDataObject;\n } else if (resource === 'like') {\n const subjectUri = this.getNodeParameter('subjectUri', i) as string;\n result = (await likePost(\n agent,\n subjectUri,\n )) as unknown as IDataObject;\n } else if (resource === 'repost') {\n const subjectUri = this.getNodeParameter('subjectUri', i) as string;\n result = (await repostPost(\n agent,\n subjectUri,\n )) as unknown as IDataObject;\n } else if (resource === 'follow') {\n const subjectUser = this.getNodeParameter(\n 'subjectUser',\n i,\n ) as string;\n result = (await followUser(\n agent,\n subjectUser,\n )) as unknown as IDataObject;\n } else {\n throw new NodeOperationError(\n this.getNode(),\n `Unsupported resource/operation: ${resource}/${operation}`,\n { itemIndex: i },\n );\n }\n\n returnData.push({\n json: result,\n pairedItem: { item: i },\n });\n } catch (error) {\n if (this.continueOnFail()) {\n returnData.push({\n json: {\n error: error instanceof Error ? error.message : String(error),\n },\n pairedItem: { item: i },\n });\n continue;\n }\n throw new NodeOperationError(\n this.getNode(),\n error instanceof Error ? error : new Error(String(error)),\n { itemIndex: i },\n );\n }\n }\n\n return [returnData];\n }\n}\n\n/**\n * Normalize a user-supplied \"en,is,fr\" string into a string[] for the\n * record's `langs` field. Returns undefined for empty input so the\n * caller falls back to its own default.\n */\nfunction parseLangs(input: string | undefined): string[] | undefined {\n if (!input) return undefined;\n const parts = input\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n return parts.length > 0 ? parts : undefined;\n}\n"],"mappings":";;;;AAoBA,IAAM,YAAY;AAClB,IAAM,cACJ;AAEF,SAAgB,aAAa,OAAwB;CACnD,MAAM,UAAU,MAAM,KAAK;CAE3B,MAAM,UAAU,QAAQ,MAAM,SAAS;CACvC,IAAI,SACF,OAAO;EAAE,MAAM,QAAQ;EAAI,YAAY,QAAQ;EAAI,MAAM,QAAQ;CAAG;CAGtE,MAAM,WAAW,QAAQ,MAAM,WAAW;CAC1C,IAAI,UACF,OAAO;EACL,MAAM,SAAS;EACf,YAAY;EACZ,MAAM,SAAS;CACjB;CAGF,MAAM,IAAI,MACR,mCAAmC,MAAM,yEAC3C;AACF;;;;;AAMA,eAAsB,aACpB,OACA,KACuE;CACvE,IAAI,OAAO,IAAI;CACf,IAAI,CAAC,KAAK,WAAW,MAAM,GAIzB,QAAO,MAHgB,MAAM,IAAI,QAAQ,SAAS,cAAc,EAC9D,QAAQ,KACV,CAAC,GACe,KAAK;CAGvB,MAAM,MAAM,MAAM,MAAM,IAAI,QAAQ,KAAK,UAAU;EACjD;EACA,YAAY,IAAI;EAChB,MAAM,IAAI;CACZ,CAAC;CAED,OAAO;EACL,KAAK,IAAI,KAAK;EACd,KAAK,IAAI,KAAK,OAAO;EACrB,OAAO,IAAI,KAAK;CAClB;AACF;;;;;;;;;;AC9BA,eAAe,cACb,OACA,MAC+C;CAC/C,MAAM,KAAK,IAAI,eAAA,SAAS,EAAE,KAAK,CAAC;CAChC,MAAM,GAAG,aAAa,KAAK;CAC3B,OAAO;EACL,MAAM,GAAG;EACT,GAAI,GAAG,UAAU,GAAG,OAAO,SAAS,IAAI,EAAE,QAAQ,GAAG,OAAO,IAAI,CAAC;CACnE;AACF;AAaA,eAAsB,WACpB,OACA,QACuB;CAGvB,MAAM,SAAkC;EACtC,OAAO;EACP,GAAG,MAJY,cAAc,OAAO,OAAO,IAAI;EAK/C,OAAO,OAAO,SAAS,OAAO,MAAM,SAAS,IAAI,OAAO,QAAQ,CAAC,IAAI;EACrE,4BAAW,IAAI,KAAK,GAAE,YAAY;CACpC;CAEA,IAAI,OAAO,OACT,OAAO,QAAQ;EACb,OAAO;EACP,QAAQ,CAAC;GAAE,KAAK,OAAO,MAAM;GAAK,OAAO,OAAO,MAAM;EAAK,CAAC;CAC9D;CAGF,MAAM,MAAM,MAAM,MAAM,IAAI,QAAQ,KAAK,aAAa;EACpD,MAAM,MAAM;EACZ,YAAY;EACZ;CACF,CAAC;CAED,OAAO;EAAE,KAAK,IAAI,KAAK;EAAK,KAAK,IAAI,KAAK;CAAI;AAChD;;;;;;AAiBA,eAAsB,YACpB,OACA,QACuB;CAEvB,MAAM,SAAS,MAAM,aAAa,OADhB,aAAa,OAAO,SACG,CAAS;CAElD,MAAM,kBAA6B;EAAE,KAAK,OAAO;EAAK,KAAK,OAAO;CAAI;CAEtE,MAAM,OADe,OAAO,MAA2C,OAClC,QAAQ;CAI7C,MAAM,SAAS;EACb,OAAO;EACP,GAAG,MAJY,cAAc,OAAO,OAAO,IAAI;EAK/C,OACE,OAAO,SAAS,OAAO,MAAM,SAAS,IAAI,OAAO,QAAQ,CAAC,IAAI;EAChE,OAAO;GAAE;GAAM,QAAQ;EAAgB;EACvC,4BAAW,IAAI,KAAK,GAAE,YAAY;CACpC;CAEA,MAAM,MAAM,MAAM,MAAM,IAAI,QAAQ,KAAK,aAAa;EACpD,MAAM,MAAM;EACZ,YAAY;EACZ;CACF,CAAC;CAED,OAAO;EAAE,KAAK,IAAI,KAAK;EAAK,KAAK,IAAI,KAAK;CAAI;AAChD;AAYA,eAAsB,UACpB,OACA,QACuB;CAEvB,MAAM,UAAU,MAAM,aAAa,OADvB,aAAa,OAAO,SACU,CAAG;CAI7C,MAAM,SAAS;EACb,OAAO;EACP,GAAG,MAJY,cAAc,OAAO,OAAO,IAAI;EAK/C,OACE,OAAO,SAAS,OAAO,MAAM,SAAS,IAAI,OAAO,QAAQ,CAAC,IAAI;EAChE,OAAO;GACL,OAAO;GACP,QAAQ;IAAE,KAAK,QAAQ;IAAK,KAAK,QAAQ;GAAI;EAC/C;EACA,4BAAW,IAAI,KAAK,GAAE,YAAY;CACpC;CAEA,MAAM,MAAM,MAAM,MAAM,IAAI,QAAQ,KAAK,aAAa;EACpD,MAAM,MAAM;EACZ,YAAY;EACZ;CACF,CAAC;CAED,OAAO;EAAE,KAAK,IAAI,KAAK;EAAK,KAAK,IAAI,KAAK;CAAI;AAChD;AAMA,eAAe,iBACb,OACA,SACA,YACuB;CAEvB,MAAM,UAAU,MAAM,aAAa,OADvB,aAAa,OACiB,CAAG;CAE7C,MAAM,MAAM,MAAM,MAAM,IAAI,QAAQ,KAAK,aAAa;EACpD,MAAM,MAAM;EACZ;EACA,QAAQ;GACN,OAAO;GACP,SAAS;IAAE,KAAK,QAAQ;IAAK,KAAK,QAAQ;GAAI;GAC9C,4BAAW,IAAI,KAAK,GAAE,YAAY;EACpC;CACF,CAAC;CAED,OAAO;EAAE,KAAK,IAAI,KAAK;EAAK,KAAK,IAAI,KAAK;CAAI;AAChD;AAEA,SAAgB,SACd,OACA,SACuB;CACvB,OAAO,iBAAiB,OAAO,SAAS,oBAAoB;AAC9D;AAEA,SAAgB,WACd,OACA,SACuB;CACvB,OAAO,iBAAiB,OAAO,SAAS,sBAAsB;AAChE;;;;;AAUA,eAAsB,WACpB,OACA,aACuB;CAEvB,IAAI,MADY,YAAY,KAClB;CACV,IAAI,CAAC,IAAI,WAAW,MAAM,GAAG;EAE3B,MAAM,SAAS,IAAI,WAAW,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI;EAIpD,OAAM,MAHiB,MAAM,IAAI,QAAQ,SAAS,cAAc,EAC9D,OACF,CAAC,GACc,KAAK;CACtB;CAEA,MAAM,MAAM,MAAM,MAAM,IAAI,QAAQ,KAAK,aAAa;EACpD,MAAM,MAAM;EACZ,YAAY;EACZ,QAAQ;GACN,OAAO;GACP,SAAS;GACT,4BAAW,IAAI,KAAK,GAAE,YAAY;EACpC;CACF,CAAC;CAED,OAAO;EAAE,KAAK,IAAI,KAAK;EAAK,KAAK,IAAI,KAAK;CAAI;AAChD;;;ACjOA,IAAa,iBAAb,MAAiD;CAC/C,cAAoC;EAClC,aAAa;EACb,MAAM;EACN,MAAM;EACN,OAAO,CAAC,WAAW;EACnB,SAAS;EACT,UACE;EACF,aAAa;EACb,UAAU,EACR,MAAM,UACR;EACA,cAAc;EACd,QAAQ,CAAC,aAAA,oBAAoB,IAAI;EACjC,SAAS,CAAC,aAAA,oBAAoB,IAAI;EAClC,aAAa,CACX;GACE,MAAM;GACN,UAAU;EACZ,CACF;EACA,YAAY;GAIV;IACE,aAAa;IACb,MAAM;IACN,MAAM;IACN,kBAAkB;IAElB,SAAS;KACP;MAAE,MAAM;MAAU,OAAO;KAAS;KAClC;MAAE,MAAM;MAAQ,OAAO;KAAO;KAC9B;MAAE,MAAM;MAAQ,OAAO;KAAO;KAC9B;MAAE,MAAM;MAAU,OAAO;KAAS;IACpC;IACA,SAAS;GACX;GAKA;IACE,aAAa;IACb,MAAM;IACN,MAAM;IACN,kBAAkB;IAClB,gBAAgB,EACd,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,EAC7B;IAEA,SAAS;KACP;MACE,MAAM;MACN,OAAO;MACP,aAAa;MACb,QAAQ;KACV;KACA;MACE,MAAM;MACN,OAAO;MACP,aAAa;MACb,QAAQ;KACV;KACA;MACE,MAAM;MACN,OAAO;MACP,aAAa;MACb,QAAQ;KACV;IACF;IACA,SAAS;GACX;GAKA;IACE,aAAa;IACb,MAAM;IACN,MAAM;IACN,kBAAkB;IAClB,gBAAgB,EACd,MAAM,EAAE,UAAU;KAAC;KAAQ;KAAU;IAAQ,EAAE,EACjD;IACA,SAAS,CACP;KACE,MAAM;KACN,OAAO;KACP,QAAQ;IACV,CACF;IACA,SAAS;GACX;GAKA;IACE,aAAa;IACb,MAAM;IACN,MAAM;IACN,UAAU;IACV,SAAS;IACT,aAAa;IACb,aACE;IACF,aAAa,EACX,MAAM,EACR;IACA,gBAAgB,EACd,MAAM;KACJ,UAAU,CAAC,MAAM;KACjB,WAAW;MAAC;MAAU;MAAS;KAAO;IACxC,EACF;GACF;GAKA;IACE,aAAa;IACb,MAAM;IACN,MAAM;IACN,UAAU;IACV,SAAS;IACT,aACE;IACF,aACE;IACF,gBAAgB,EACd,MAAM;KACJ,UAAU,CAAC,MAAM;KACjB,WAAW,CAAC,OAAO;IACrB,EACF;GACF;GAKA;IACE,aAAa;IACb,MAAM;IACN,MAAM;IACN,UAAU;IACV,SAAS;IACT,aACE;IACF,aACE;IACF,gBAAgB,EACd,MAAM;KACJ,UAAU,CAAC,MAAM;KACjB,WAAW,CAAC,OAAO;IACrB,EACF;GACF;GAKA;IACE,aAAa;IACb,MAAM;IACN,MAAM;IACN,UAAU;IACV,SAAS;IACT,aACE;IACF,aAAa;IACb,gBAAgB,EACd,MAAM,EACJ,UAAU,CAAC,QAAQ,QAAQ,EAC7B,EACF;GACF;GAKA;IACE,aAAa;IACb,MAAM;IACN,MAAM;IACN,UAAU;IACV,SAAS;IACT,aAAa;IACb,aAAa;IACb,gBAAgB,EACd,MAAM,EACJ,UAAU,CAAC,QAAQ,EACrB,EACF;GACF;GAKA;IACE,aAAa;IACb,MAAM;IACN,MAAM;IACN,aAAa;IACb,SAAS,CAAC;IACV,gBAAgB,EACd,MAAM;KACJ,UAAU,CAAC,MAAM;KACjB,WAAW;MAAC;MAAU;MAAS;KAAO;IACxC,EACF;IACA,SAAS;KACP;MACE,aAAa;MACb,MAAM;MACN,MAAM;MACN,SAAS;MACT,aAAa;MACb,aACE;KACJ;KACA;MACE,aAAa;MACb,MAAM;MACN,MAAM;MACN,SAAS;MACT,aAAa;MACb,aACE;MACF,gBAAgB,EACd,MAAM,EACJ,cAAc,CAAC,QAAQ,EACzB,EACF;KACF;KACA;MACE,aAAa;MACb,MAAM;MACN,MAAM;MACN,SAAS;MACT,aAAa;MACb,gBAAgB,EACd,MAAM,EACJ,cAAc,CAAC,QAAQ,EACzB,EACF;KACF;IACF;GACF;EACF;CACF;CAEA,MAAM,UAAiC;EACrC,MAAM,QAAQ,KAAK,aAAa;EAChC,MAAM,aAAmC,CAAC;EAG1C,MAAM,QAAQ,MAAM,eAAA,YAAY,MADN,KAAK,eAAe,YAAY,CACA;EAE1D,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAChC,IAAI;GACF,MAAM,WAAW,KAAK,iBAAiB,YAAY,CAAC;GACpD,MAAM,YAAY,KAAK,iBAAiB,aAAa,CAAC;GAEtD,IAAI;GAEJ,IAAI,aAAa,UAAU,cAAc,UAAU;IACjD,MAAM,OAAO,KAAK,iBAAiB,QAAQ,CAAC;IAC5C,MAAM,UAAU,KAAK,iBACnB,WACA,GACA,CAAC,CACH;IAMA,MAAM,QAAQ,WAAW,QAAQ,KAAK;IACtC,IAAI;IAEJ,IAAI,QAAQ,qBAAqB;KAC/B,MAAM,aAAa,MAAM,KAAK,QAAQ,oBACpC,GACA,QAAQ,mBACV;KACA,MAAM,WACJ,MAAM,GAAG,SAAS,QAAQ,sBAAsB,YAChD;KAKF,QAAQ;MACN,OAAM,MALe,MAAM,IAAI,QAAQ,KAAK,WAC5C,YACA,EAAE,UAAU,SAAS,CACvB,GAEiB,KAAK;MACpB,KAAK,QAAQ,YAAY;KAC3B;IACF;IAEA,SAAU,MAAM,WAAW,OAAO;KAChC;KACA;KACA;IACF,CAAC;GACH,OAAO,IAAI,aAAa,UAAU,cAAc,SAO9C,SAAU,MAAM,YAAY,OAAO;IACjC,MAPW,KAAK,iBAAiB,QAAQ,CAOzC;IACA,WAPgB,KAAK,iBAAiB,aAAa,CAOnD;IACA,OAAO,WAPO,KAAK,iBAAiB,WAAW,GAAG,CAAC,CAOjC,EAAQ,KAAK;GACjC,CAAC;QACI,IAAI,aAAa,UAAU,cAAc,SAO9C,SAAU,MAAM,UAAU,OAAO;IAC/B,MAPW,KAAK,iBAAiB,QAAQ,CAOzC;IACA,WAPgB,KAAK,iBAAiB,aAAa,CAOnD;IACA,OAAO,WAPO,KAAK,iBAAiB,WAAW,GAAG,CAAC,CAOjC,EAAQ,KAAK;GACjC,CAAC;QACI,IAAI,aAAa,QAEtB,SAAU,MAAM,SACd,OAFiB,KAAK,iBAAiB,cAAc,CAGrD,CACF;QACK,IAAI,aAAa,UAEtB,SAAU,MAAM,WACd,OAFiB,KAAK,iBAAiB,cAAc,CAGrD,CACF;QACK,IAAI,aAAa,UAKtB,SAAU,MAAM,WACd,OALkB,KAAK,iBACvB,eACA,CAIA,CACF;QAEA,MAAM,IAAI,aAAA,mBACR,KAAK,QAAQ,GACb,mCAAmC,SAAS,GAAG,aAC/C,EAAE,WAAW,EAAE,CACjB;GAGF,WAAW,KAAK;IACd,MAAM;IACN,YAAY,EAAE,MAAM,EAAE;GACxB,CAAC;EACH,SAAS,OAAO;GACd,IAAI,KAAK,eAAe,GAAG;IACzB,WAAW,KAAK;KACd,MAAM,EACJ,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D;KACA,YAAY,EAAE,MAAM,EAAE;IACxB,CAAC;IACD;GACF;GACA,MAAM,IAAI,aAAA,mBACR,KAAK,QAAQ,GACb,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,GACxD,EAAE,WAAW,EAAE,CACjB;EACF;EAGF,OAAO,CAAC,UAAU;CACpB;AACF;;;;;;AAOA,SAAS,WAAW,OAAiD;CACnE,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,MAAM,QAAQ,MACX,MAAM,GAAG,EACT,KAAK,MAAM,EAAE,KAAK,CAAC,EACnB,QAAQ,MAAM,EAAE,SAAS,CAAC;CAC7B,OAAO,MAAM,SAAS,IAAI,QAAQ,KAAA;AACpC"}
1
+ {"version":3,"file":"AtprotoBluesky.node.js","names":[],"sources":["../../../nodes/Bluesky/postUri.ts","../../../nodes/Bluesky/operations.ts","../../../nodes/Bluesky/external.ts","../../../nodes/Bluesky/AtprotoBluesky.node.ts"],"sourcesContent":["/**\n * Parse a Bluesky post reference into its repo/collection/rkey parts.\n *\n * Accepts either an AT-URI or an HTTPS bsky.app URL:\n * at://did:plc:abc.../app.bsky.feed.post/3jzfcijpj2z2a\n * https://bsky.app/profile/handle.bsky.social/post/3jzfcijpj2z2a\n *\n * Returns `{ repo, collection, rkey }` where `repo` is a DID or handle.\n * For the HTTPS form, `repo` is the handle from the URL — call\n * `agent.com.atproto.identity.resolveHandle` if you need a DID.\n */\n\nimport type { Agent } from '@atproto/api';\n\nexport interface PostRef {\n repo: string;\n collection: string;\n rkey: string;\n}\n\nconst AT_URI_RE = /^at:\\/\\/([^/]+)\\/([^/]+)\\/([^/]+)$/;\nconst BSKY_URL_RE =\n /^https?:\\/\\/bsky\\.app\\/profile\\/([^/]+)\\/post\\/([^/?#]+)/;\n\nexport function parsePostUri(input: string): PostRef {\n const trimmed = input.trim();\n\n const atMatch = trimmed.match(AT_URI_RE);\n if (atMatch) {\n return { repo: atMatch[1], collection: atMatch[2], rkey: atMatch[3] };\n }\n\n const urlMatch = trimmed.match(BSKY_URL_RE);\n if (urlMatch) {\n return {\n repo: urlMatch[1],\n collection: 'app.bsky.feed.post',\n rkey: urlMatch[2],\n };\n }\n\n throw new Error(\n `Could not parse post reference '${input}'. Expected an at:// URI or a https://bsky.app/profile/.../post/... URL.`,\n );\n}\n\n/**\n * Resolve a `PostRef` to its actual `{ uri, cid }` by calling getRecord.\n * Handles the HTTPS-URL case where `repo` is a handle (resolves to DID first).\n */\nexport async function fetchPostRef(\n agent: Agent,\n ref: PostRef,\n): Promise<{ uri: string; cid: string; value: Record<string, unknown> }> {\n let repo = ref.repo;\n if (!repo.startsWith('did:')) {\n const resolved = await agent.com.atproto.identity.resolveHandle({\n handle: repo,\n });\n repo = resolved.data.did;\n }\n\n const res = await agent.com.atproto.repo.getRecord({\n repo,\n collection: ref.collection,\n rkey: ref.rkey,\n });\n\n return {\n uri: res.data.uri,\n cid: res.data.cid ?? '',\n value: res.data.value as Record<string, unknown>,\n };\n}\n","/**\n * Bluesky-specific operations. Thin wrappers around the AT Protocol\n * createRecord XRPC that handle the per-record-type ceremony:\n *\n * - Post.create → builds the rich-text record (auto-facet detection),\n * optional language tags, optional image embed\n * - Post.reply → walks parent → root and builds the reply ref\n * - Post.quote → builds an `app.bsky.embed.record` embed\n * - Like.create → fetches subject CID, creates `app.bsky.feed.like`\n * - Repost.create → same shape as Like, in `app.bsky.feed.repost`\n * - Follow.create → resolves handle → DID, creates `app.bsky.graph.follow`\n *\n * All operations return `{ uri, cid }` from the underlying createRecord\n * call so the workflow can chain on the new record.\n */\n\nimport type { Agent } from '@atproto/api';\nimport { RichText } from '@atproto/api';\n\nimport { fetchPostRef, parsePostUri } from './postUri';\n\n// ---------------------------------------------------------------------------\n// Common types\n// ---------------------------------------------------------------------------\n\ntype StrongRef = { uri: string; cid: string };\n\nexport interface CreateResult {\n uri: string;\n cid: string;\n}\n\n// ---------------------------------------------------------------------------\n// Rich-text builder — used by Create / Reply / Quote\n// ---------------------------------------------------------------------------\n\n/**\n * Build the rich-text payload for a post: `{ text, facets }`.\n *\n * Uses `RichText.detectFacets(agent)` from @atproto/api, which finds\n * @mentions (and resolves them to DIDs via the PDS), URLs and #tags,\n * with correct byte offsets per the lexicon.\n */\nasync function buildRichText(\n agent: Agent,\n text: string,\n): Promise<{ text: string; facets?: unknown[] }> {\n const rt = new RichText({ text });\n await rt.detectFacets(agent);\n return {\n text: rt.text,\n ...(rt.facets && rt.facets.length > 0 ? { facets: rt.facets } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Post — Create\n// ---------------------------------------------------------------------------\n\nexport interface CreatePostParams {\n text: string;\n langs?: string[];\n /** Optional image embed: pre-uploaded blob + alt text */\n image?: { blob: unknown; alt: string };\n /** Optional external link card. `thumb` is a pre-uploaded blob. */\n external?: {\n uri: string;\n title: string;\n description: string;\n thumb?: unknown;\n };\n}\n\nexport async function createPost(\n agent: Agent,\n params: CreatePostParams,\n): Promise<CreateResult> {\n const rt = await buildRichText(agent, params.text);\n\n const record: Record<string, unknown> = {\n $type: 'app.bsky.feed.post',\n ...rt,\n langs: params.langs && params.langs.length > 0 ? params.langs : ['en'],\n createdAt: new Date().toISOString(),\n };\n\n if (params.image && params.external) {\n throw new Error(\n 'A post can carry either an image embed or an external link card, not both.',\n );\n }\n\n if (params.image) {\n record.embed = {\n $type: 'app.bsky.embed.images',\n images: [{ alt: params.image.alt, image: params.image.blob }],\n };\n } else if (params.external) {\n const { uri, title, description, thumb } = params.external;\n record.embed = {\n $type: 'app.bsky.embed.external',\n external: { uri, title, description, ...(thumb ? { thumb } : {}) },\n };\n }\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection: 'app.bsky.feed.post',\n record,\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n\n// ---------------------------------------------------------------------------\n// Post — Reply\n// ---------------------------------------------------------------------------\n\nexport interface ReplyParams {\n parentUri: string;\n text: string;\n langs?: string[];\n}\n\n/**\n * Reply to a post. Walks the parent record to discover the thread root —\n * if the parent is itself a reply, we reuse its `reply.root`; otherwise\n * the parent IS the root.\n */\nexport async function replyToPost(\n agent: Agent,\n params: ReplyParams,\n): Promise<CreateResult> {\n const parentRef = parsePostUri(params.parentUri);\n const parent = await fetchPostRef(agent, parentRef);\n\n const parentStrongRef: StrongRef = { uri: parent.uri, cid: parent.cid };\n const parentReply = (parent.value as { reply?: { root?: StrongRef } }).reply;\n const root: StrongRef = parentReply?.root ?? parentStrongRef;\n\n const rt = await buildRichText(agent, params.text);\n\n const record = {\n $type: 'app.bsky.feed.post',\n ...rt,\n langs:\n params.langs && params.langs.length > 0 ? params.langs : ['en'],\n reply: { root, parent: parentStrongRef },\n createdAt: new Date().toISOString(),\n };\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection: 'app.bsky.feed.post',\n record,\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n\n// ---------------------------------------------------------------------------\n// Post — Quote\n// ---------------------------------------------------------------------------\n\nexport interface QuoteParams {\n quotedUri: string;\n text: string;\n langs?: string[];\n}\n\nexport async function quotePost(\n agent: Agent,\n params: QuoteParams,\n): Promise<CreateResult> {\n const ref = parsePostUri(params.quotedUri);\n const subject = await fetchPostRef(agent, ref);\n\n const rt = await buildRichText(agent, params.text);\n\n const record = {\n $type: 'app.bsky.feed.post',\n ...rt,\n langs:\n params.langs && params.langs.length > 0 ? params.langs : ['en'],\n embed: {\n $type: 'app.bsky.embed.record',\n record: { uri: subject.uri, cid: subject.cid },\n },\n createdAt: new Date().toISOString(),\n };\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection: 'app.bsky.feed.post',\n record,\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n\n// ---------------------------------------------------------------------------\n// Engagement — Like / Repost\n// ---------------------------------------------------------------------------\n\nasync function createSubjectRef(\n agent: Agent,\n postUri: string,\n collection: 'app.bsky.feed.like' | 'app.bsky.feed.repost',\n): Promise<CreateResult> {\n const ref = parsePostUri(postUri);\n const subject = await fetchPostRef(agent, ref);\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection,\n record: {\n $type: collection,\n subject: { uri: subject.uri, cid: subject.cid },\n createdAt: new Date().toISOString(),\n },\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n\nexport function likePost(\n agent: Agent,\n postUri: string,\n): Promise<CreateResult> {\n return createSubjectRef(agent, postUri, 'app.bsky.feed.like');\n}\n\nexport function repostPost(\n agent: Agent,\n postUri: string,\n): Promise<CreateResult> {\n return createSubjectRef(agent, postUri, 'app.bsky.feed.repost');\n}\n\n// ---------------------------------------------------------------------------\n// Graph — Follow\n// ---------------------------------------------------------------------------\n\n/**\n * Follow a user. Accepts either a handle (`alice.bsky.social`) or a DID.\n * Handles are resolved via `com.atproto.identity.resolveHandle` first.\n */\nexport async function followUser(\n agent: Agent,\n handleOrDid: string,\n): Promise<CreateResult> {\n const trimmed = handleOrDid.trim();\n let did = trimmed;\n if (!did.startsWith('did:')) {\n // Strip leading @ if the user typed @alice.bsky.social\n const handle = did.startsWith('@') ? did.slice(1) : did;\n const resolved = await agent.com.atproto.identity.resolveHandle({\n handle,\n });\n did = resolved.data.did;\n }\n\n const res = await agent.com.atproto.repo.createRecord({\n repo: agent.did!,\n collection: 'app.bsky.graph.follow',\n record: {\n $type: 'app.bsky.graph.follow',\n subject: did,\n createdAt: new Date().toISOString(),\n },\n });\n\n return { uri: res.data.uri, cid: res.data.cid };\n}\n","/**\n * OpenGraph scraping for Bluesky external link-card embeds\n * (`app.bsky.embed.external`).\n *\n * A link card is built from a page's `og:*` (and `twitter:*`) meta tags.\n * We fetch the target HTML and extract title / description / image with a\n * small, dependency-free `<meta>` scanner — the package ships zero runtime\n * deps, so a full HTML parser is out of scope. This handles the `<head>`\n * meta tags every card relies on; it is not a general HTML parser.\n */\n\nexport interface ExternalMetadata {\n title: string;\n description: string;\n /** Absolute URL of the card thumbnail, if the page advertises one. */\n image?: string;\n}\n\n/** Minimal slice of the Fetch `Response` this module consumes. */\nexport interface HttpResponse {\n ok: boolean;\n status: number;\n statusText: string;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n headers: { get(name: string): string | null };\n}\n\nexport type HttpFetch = (\n url: string,\n init?: { headers?: Record<string, string> },\n) => Promise<HttpResponse>;\n\nconst USER_AGENT = 'n8n-nodes-atproto (link-card scraper)';\n\n// Typed wrapper over the global fetch (Node >=22, see package.json engines).\n// The wrapper form keeps the call site fully typed without an inline cast.\nconst defaultFetch: HttpFetch = (url, init) => fetch(url, init);\n\nconst errMessage = (err: unknown): string =>\n err instanceof Error ? err.message : String(err);\n\n// ---------------------------------------------------------------------------\n// Network\n// ---------------------------------------------------------------------------\n\n/**\n * Fetch a URL and extract its link-card metadata. Throws if the page cannot\n * be retrieved (network error or non-2xx) — the caller treats a failed scrape\n * as a hard failure of the item.\n */\nexport async function fetchExternalMetadata(\n url: string,\n doFetch: HttpFetch = defaultFetch,\n): Promise<ExternalMetadata> {\n let res: HttpResponse | undefined;\n let failure: string | undefined;\n try {\n res = await doFetch(url, {\n headers: {\n 'user-agent': USER_AGENT,\n accept: 'text/html,application/xhtml+xml',\n },\n });\n } catch (err) {\n failure = errMessage(err);\n }\n if (failure !== undefined) {\n throw new Error(\n `Could not fetch ${url} for link-card metadata: ${failure}`,\n );\n }\n if (!res || !res.ok) {\n const detail = res ? `HTTP ${res.status} ${res.statusText}` : 'no response';\n throw new Error(\n `Could not fetch ${url} for link-card metadata: ${detail}`,\n );\n }\n return parseOpenGraph(await res.text(), url);\n}\n\n/**\n * Download a thumbnail image. Returns the raw bytes and the response's\n * content-type. Throws on a non-2xx response; the caller treats the thumbnail\n * as best-effort and posts without it on failure.\n */\nexport async function fetchThumbnail(\n url: string,\n doFetch: HttpFetch = defaultFetch,\n): Promise<{ bytes: Uint8Array; mimeType: string }> {\n const res = await doFetch(url, { headers: { 'user-agent': USER_AGENT } });\n if (!res.ok) {\n throw new Error(`HTTP ${res.status} ${res.statusText}`);\n }\n const bytes = new Uint8Array(await res.arrayBuffer());\n const contentType = res.headers.get('content-type');\n const mimeType = contentType\n ? contentType.split(';')[0].trim()\n : 'application/octet-stream';\n return { bytes, mimeType };\n}\n\n// ---------------------------------------------------------------------------\n// Parsing\n// ---------------------------------------------------------------------------\n\nconst META_TAG = /<meta\\b[^>]*>/gi;\nconst TITLE_TAG = /<title[^>]*>([\\s\\S]*?)<\\/title>/i;\n\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n nbsp: ' ',\n};\n\nfunction decodeEntities(input: string): string {\n return input.replace(\n /&(#x?[0-9a-f]+|[a-z][a-z0-9]*);/gi,\n (whole, body: string) => {\n if (body[0] === '#') {\n const code =\n body[1].toLowerCase() === 'x'\n ? parseInt(body.slice(2), 16)\n : parseInt(body.slice(1), 10);\n return Number.isFinite(code) && code >= 0 && code <= 0x10ffff\n ? String.fromCodePoint(code)\n : whole;\n }\n return NAMED_ENTITIES[body.toLowerCase()] ?? whole;\n },\n );\n}\n\n/** Read an attribute value (double- or single-quoted) from a single tag. */\nfunction getAttr(tag: string, name: string): string | undefined {\n const match = new RegExp(`\\\\b${name}\\\\s*=\\\\s*(\"([^\"]*)\"|'([^']*)')`, 'i').exec(\n tag,\n );\n if (!match) return undefined;\n return match[2] ?? match[3];\n}\n\n/**\n * Extract link-card metadata from page HTML.\n *\n * Precedence: `og:*` → `twitter:*` → `<title>` / `<meta name=\"description\">`.\n * A relative `og:image` is resolved against `baseUrl`. Returns empty strings\n * for missing title/description (both are required by the lexicon).\n */\nexport function parseOpenGraph(html: string, baseUrl: string): ExternalMetadata {\n const props = new Map<string, string>();\n for (const tag of html.match(META_TAG) ?? []) {\n const key = getAttr(tag, 'property') ?? getAttr(tag, 'name');\n const content = getAttr(tag, 'content');\n if (key && content !== undefined && !props.has(key.toLowerCase())) {\n props.set(key.toLowerCase(), content);\n }\n }\n\n const pick = (...keys: string[]): string | undefined => {\n for (const key of keys) {\n const value = props.get(key);\n if (value !== undefined && value !== '') return value;\n }\n return undefined;\n };\n\n let title = pick('og:title', 'twitter:title');\n if (title === undefined) {\n const match = TITLE_TAG.exec(html);\n if (match) title = match[1].trim();\n }\n\n const description =\n pick('og:description', 'twitter:description', 'description') ?? '';\n\n const imageRaw = pick('og:image', 'og:image:url', 'twitter:image');\n let image: string | undefined;\n if (imageRaw) {\n try {\n image = new URL(decodeEntities(imageRaw), baseUrl).toString();\n } catch {\n image = undefined;\n }\n }\n\n return {\n title: decodeEntities(title ?? ''),\n description: decodeEntities(description),\n image,\n };\n}\n","/**\n * Bluesky node — opinionated, task-shaped wrappers around the\n * AT Protocol `app.bsky.*` lexicons. Hides NSIDs and lexicon details\n * for the most common Bluesky tasks: posting, replying, quoting,\n * liking, reposting and following.\n *\n * Power users should keep using the generic AT Protocol node for\n * anything not covered here, or for non-default collection NSIDs.\n */\n\nimport type {\n IDataObject,\n IExecuteFunctions,\n INodeExecutionData,\n INodeType,\n INodeTypeDescription,\n} from 'n8n-workflow';\nimport { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';\n\nimport { createAgent } from '../Atproto/shared';\nimport {\n createPost,\n followUser,\n likePost,\n quotePost,\n replyToPost,\n repostPost,\n} from './operations';\nimport {\n fetchExternalMetadata,\n fetchThumbnail,\n} from './external';\n\nexport class AtprotoBluesky implements INodeType {\n description: INodeTypeDescription = {\n displayName: 'Bluesky',\n name: 'atprotoBluesky',\n icon: 'file:bluesky.svg',\n group: ['transform'],\n version: 1,\n subtitle:\n '={{ $parameter[\"operation\"] + \": \" + $parameter[\"resource\"] }}',\n description: 'Post, reply, quote, like, repost and follow on Bluesky',\n defaults: {\n name: 'Bluesky',\n },\n usableAsTool: true,\n inputs: [NodeConnectionTypes.Main],\n outputs: [NodeConnectionTypes.Main],\n credentials: [\n {\n name: 'atprotoApi',\n required: true,\n },\n ],\n properties: [\n // ------------------------------------------------------------------\n // Resource\n // ------------------------------------------------------------------\n {\n displayName: 'Resource',\n name: 'resource',\n type: 'options',\n noDataExpression: true,\n // Sorted alphabetically per @n8n/community-nodes lint rule.\n options: [\n { name: 'Follow', value: 'follow' },\n { name: 'Like', value: 'like' },\n { name: 'Post', value: 'post' },\n { name: 'Repost', value: 'repost' },\n ],\n default: 'post',\n },\n\n // ------------------------------------------------------------------\n // Operation — Post\n // ------------------------------------------------------------------\n {\n displayName: 'Operation',\n name: 'operation',\n type: 'options',\n noDataExpression: true,\n displayOptions: {\n show: { resource: ['post'] },\n },\n // Sorted alphabetically per @n8n/community-nodes lint rule.\n options: [\n {\n name: 'Create',\n value: 'create',\n description: 'Create a new post',\n action: 'Create a post',\n },\n {\n name: 'Quote',\n value: 'quote',\n description: 'Quote-post an existing post',\n action: 'Quote a post',\n },\n {\n name: 'Reply',\n value: 'reply',\n description: 'Reply to an existing post',\n action: 'Reply to a post',\n },\n ],\n default: 'create',\n },\n\n // ------------------------------------------------------------------\n // Operation — Like / Repost / Follow (single operation, hidden)\n // ------------------------------------------------------------------\n {\n displayName: 'Operation',\n name: 'operation',\n type: 'options',\n noDataExpression: true,\n displayOptions: {\n show: { resource: ['like', 'repost', 'follow'] },\n },\n options: [\n {\n name: 'Create',\n value: 'create',\n action: 'Create',\n },\n ],\n default: 'create',\n },\n\n // ------------------------------------------------------------------\n // Text — Post Create / Reply / Quote\n // ------------------------------------------------------------------\n {\n displayName: 'Text',\n name: 'text',\n type: 'string',\n required: true,\n default: '',\n placeholder: 'Hello @alice.bsky.social, check this out!',\n description:\n 'The post text. Mentions (@handle), URLs and #hashtags are auto-detected and rendered as rich-text facets.',\n typeOptions: {\n rows: 4,\n },\n displayOptions: {\n show: {\n resource: ['post'],\n operation: ['create', 'reply', 'quote'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // Parent URI — Reply\n // ------------------------------------------------------------------\n {\n displayName: 'Reply To',\n name: 'parentUri',\n type: 'string',\n required: true,\n default: '',\n placeholder:\n 'at://did:plc:.../app.bsky.feed.post/3jzfc... or https://bsky.app/profile/.../post/3jzfc...',\n description:\n 'The post to reply to. Accepts either an at:// URI or a bsky.app URL.',\n displayOptions: {\n show: {\n resource: ['post'],\n operation: ['reply'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // Quoted URI — Quote\n // ------------------------------------------------------------------\n {\n displayName: 'Quoted Post',\n name: 'quotedUri',\n type: 'string',\n required: true,\n default: '',\n placeholder:\n 'at://did:plc:.../app.bsky.feed.post/3jzfc... or https://bsky.app/profile/.../post/3jzfc...',\n description:\n 'The post to quote. Accepts either an at:// URI or a bsky.app URL.',\n displayOptions: {\n show: {\n resource: ['post'],\n operation: ['quote'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // Subject — Like / Repost\n // ------------------------------------------------------------------\n {\n displayName: 'Post',\n name: 'subjectUri',\n type: 'string',\n required: true,\n default: '',\n placeholder:\n 'at://did:plc:.../app.bsky.feed.post/3jzfc... or https://bsky.app/profile/.../post/3jzfc...',\n description: 'The post to like or repost. Accepts at:// URI or bsky.app URL.',\n displayOptions: {\n show: {\n resource: ['like', 'repost'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // User — Follow\n // ------------------------------------------------------------------\n {\n displayName: 'User',\n name: 'subjectUser',\n type: 'string',\n required: true,\n default: '',\n placeholder: 'alice.bsky.social or did:plc:...',\n description: 'The handle or DID of the user to follow',\n displayOptions: {\n show: {\n resource: ['follow'],\n },\n },\n },\n\n // ------------------------------------------------------------------\n // Options — Post Create / Reply / Quote\n // ------------------------------------------------------------------\n {\n displayName: 'Options',\n name: 'options',\n type: 'collection',\n placeholder: 'Add Option',\n default: {},\n displayOptions: {\n show: {\n resource: ['post'],\n operation: ['create', 'reply', 'quote'],\n },\n },\n options: [\n {\n displayName: 'Auto-Scrape Link Metadata',\n name: 'externalAutoScrape',\n type: 'boolean',\n default: true,\n description:\n 'Whether to fetch the page OpenGraph title, description and thumbnail. The item fails if the page cannot be fetched.',\n displayOptions: {\n show: {\n '/operation': ['create'],\n },\n },\n },\n {\n displayName: 'External Link URL',\n name: 'externalUrl',\n type: 'string',\n default: '',\n placeholder: 'https://example.com/article',\n description:\n 'URL to embed as a link card below the post. Leave empty for no card.',\n displayOptions: {\n show: {\n '/operation': ['create'],\n },\n },\n },\n {\n displayName: 'Image Alt Text',\n name: 'imageAlt',\n type: 'string',\n default: '',\n description: 'Accessibility description of the embedded image',\n displayOptions: {\n show: {\n '/operation': ['create'],\n },\n },\n },\n {\n displayName: 'Image Binary Property',\n name: 'imageBinaryProperty',\n type: 'string',\n default: '',\n placeholder: 'data',\n description:\n 'Name of the binary property on the incoming item containing an image to embed. Leave empty for a text-only post.',\n displayOptions: {\n show: {\n '/operation': ['create'],\n },\n },\n },\n {\n displayName: 'Languages',\n name: 'langs',\n type: 'string',\n default: 'en',\n placeholder: 'en or en,is,fr',\n description:\n 'Comma-separated BCP-47 language tags for the post. Defaults to en.',\n },\n {\n displayName: 'Link Description',\n name: 'externalDescription',\n type: 'string',\n default: '',\n description:\n 'Card description. Overrides the scraped description when set.',\n displayOptions: {\n show: {\n '/operation': ['create'],\n },\n },\n },\n {\n displayName: 'Link Thumbnail Binary Property',\n name: 'externalThumbBinaryProperty',\n type: 'string',\n default: '',\n placeholder: 'data',\n description:\n 'Name of a binary property holding a custom card thumbnail. Overrides the scraped image.',\n displayOptions: {\n show: {\n '/operation': ['create'],\n },\n },\n },\n {\n displayName: 'Link Title',\n name: 'externalTitle',\n type: 'string',\n default: '',\n description: 'Card title. Overrides the scraped title when set.',\n displayOptions: {\n show: {\n '/operation': ['create'],\n },\n },\n },\n ],\n },\n ],\n };\n\n async execute(this: IExecuteFunctions) {\n const items = this.getInputData();\n const returnData: INodeExecutionData[] = [];\n\n const credentials = await this.getCredentials('atprotoApi');\n const agent = await createAgent(credentials as IDataObject);\n\n for (let i = 0; i < items.length; i++) {\n try {\n const resource = this.getNodeParameter('resource', i) as string;\n const operation = this.getNodeParameter('operation', i) as string;\n\n let result: IDataObject;\n\n if (resource === 'post' && operation === 'create') {\n const text = this.getNodeParameter('text', i) as string;\n const options = this.getNodeParameter(\n 'options',\n i,\n {},\n ) as {\n langs?: string;\n imageBinaryProperty?: string;\n imageAlt?: string;\n externalUrl?: string;\n externalAutoScrape?: boolean;\n externalTitle?: string;\n externalDescription?: string;\n externalThumbBinaryProperty?: string;\n };\n\n const langs = parseLangs(options.langs);\n\n if (options.imageBinaryProperty && options.externalUrl) {\n throw new NodeOperationError(\n this.getNode(),\n 'A post can carry either an image embed or an external link card, not both.',\n { itemIndex: i },\n );\n }\n\n let image: { blob: unknown; alt: string } | undefined;\n\n if (options.imageBinaryProperty) {\n const binaryData = await this.helpers.getBinaryDataBuffer(\n i,\n options.imageBinaryProperty,\n );\n const mimeType =\n items[i].binary?.[options.imageBinaryProperty]?.mimeType ??\n 'application/octet-stream';\n const uploaded = await agent.com.atproto.repo.uploadBlob(\n binaryData,\n { encoding: mimeType },\n );\n image = {\n blob: uploaded.data.blob,\n alt: options.imageAlt ?? '',\n };\n }\n\n let external:\n | { uri: string; title: string; description: string; thumb?: unknown }\n | undefined;\n\n if (options.externalUrl) {\n let title = options.externalTitle ?? '';\n let description = options.externalDescription ?? '';\n let imageUrl: string | undefined;\n\n if (options.externalAutoScrape !== false) {\n const meta = await fetchExternalMetadata(options.externalUrl);\n title = options.externalTitle || meta.title;\n description = options.externalDescription || meta.description;\n imageUrl = meta.image;\n }\n\n let thumb: unknown;\n if (options.externalThumbBinaryProperty) {\n const thumbData = await this.helpers.getBinaryDataBuffer(\n i,\n options.externalThumbBinaryProperty,\n );\n const mimeType =\n items[i].binary?.[options.externalThumbBinaryProperty]\n ?.mimeType ?? 'application/octet-stream';\n const uploaded = await agent.com.atproto.repo.uploadBlob(\n thumbData,\n { encoding: mimeType },\n );\n thumb = uploaded.data.blob;\n } else if (imageUrl) {\n try {\n const { bytes, mimeType } = await fetchThumbnail(imageUrl);\n if (bytes.byteLength <= 1_000_000) {\n const uploaded = await agent.com.atproto.repo.uploadBlob(\n bytes,\n { encoding: mimeType },\n );\n thumb = uploaded.data.blob;\n }\n } catch {\n // Thumbnail is best-effort: keep the card without it.\n }\n }\n\n external = {\n uri: options.externalUrl,\n title,\n description,\n ...(thumb ? { thumb } : {}),\n };\n }\n\n result = (await createPost(agent, {\n text,\n langs,\n image,\n external,\n })) as unknown as IDataObject;\n } else if (resource === 'post' && operation === 'reply') {\n const text = this.getNodeParameter('text', i) as string;\n const parentUri = this.getNodeParameter('parentUri', i) as string;\n const options = this.getNodeParameter('options', i, {}) as {\n langs?: string;\n };\n\n result = (await replyToPost(agent, {\n text,\n parentUri,\n langs: parseLangs(options.langs),\n })) as unknown as IDataObject;\n } else if (resource === 'post' && operation === 'quote') {\n const text = this.getNodeParameter('text', i) as string;\n const quotedUri = this.getNodeParameter('quotedUri', i) as string;\n const options = this.getNodeParameter('options', i, {}) as {\n langs?: string;\n };\n\n result = (await quotePost(agent, {\n text,\n quotedUri,\n langs: parseLangs(options.langs),\n })) as unknown as IDataObject;\n } else if (resource === 'like') {\n const subjectUri = this.getNodeParameter('subjectUri', i) as string;\n result = (await likePost(\n agent,\n subjectUri,\n )) as unknown as IDataObject;\n } else if (resource === 'repost') {\n const subjectUri = this.getNodeParameter('subjectUri', i) as string;\n result = (await repostPost(\n agent,\n subjectUri,\n )) as unknown as IDataObject;\n } else if (resource === 'follow') {\n const subjectUser = this.getNodeParameter(\n 'subjectUser',\n i,\n ) as string;\n result = (await followUser(\n agent,\n subjectUser,\n )) as unknown as IDataObject;\n } else {\n throw new NodeOperationError(\n this.getNode(),\n `Unsupported resource/operation: ${resource}/${operation}`,\n { itemIndex: i },\n );\n }\n\n returnData.push({\n json: result,\n pairedItem: { item: i },\n });\n } catch (error) {\n if (this.continueOnFail()) {\n returnData.push({\n json: {\n error: error instanceof Error ? error.message : String(error),\n },\n pairedItem: { item: i },\n });\n continue;\n }\n throw new NodeOperationError(\n this.getNode(),\n error instanceof Error ? error : new Error(String(error)),\n { itemIndex: i },\n );\n }\n }\n\n return [returnData];\n }\n}\n\n/**\n * Normalize a user-supplied \"en,is,fr\" string into a string[] for the\n * record's `langs` field. Returns undefined for empty input so the\n * caller falls back to its own default.\n */\nfunction parseLangs(input: string | undefined): string[] | undefined {\n if (!input) return undefined;\n const parts = input\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n return parts.length > 0 ? parts : undefined;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-atproto",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Generic AT Protocol node for n8n — CRUD any record in any lexicon",
5
5
  "keywords": [
6
6
  "n8n-community-node-package"