@rmdes/indiekit-syndicator-bluesky 1.0.5 → 1.0.7
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/index.js +4 -1
- package/lib/bluesky.js +25 -10
- package/lib/utils.js +200 -10
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -8,7 +8,8 @@ const defaults = {
|
|
|
8
8
|
profileUrl: "https://bsky.app/profile",
|
|
9
9
|
serviceUrl: "https://bsky.social",
|
|
10
10
|
includePermalink: false,
|
|
11
|
-
syndicateExternalLikes: true, //
|
|
11
|
+
syndicateExternalLikes: true, // Enable syndication of external likes
|
|
12
|
+
syndicateExternalReposts: true, // Enable syndication of external reposts
|
|
12
13
|
checked: false,
|
|
13
14
|
};
|
|
14
15
|
|
|
@@ -23,6 +24,7 @@ export default class BlueskySyndicator {
|
|
|
23
24
|
* @param {string} [options.password] - Password
|
|
24
25
|
* @param {boolean} [options.includePermalink] - Include permalink in status
|
|
25
26
|
* @param {boolean} [options.syndicateExternalLikes] - Syndicate likes of external URLs as posts
|
|
27
|
+
* @param {boolean} [options.syndicateExternalReposts] - Syndicate reposts of external URLs as posts
|
|
26
28
|
* @param {boolean} [options.checked] - Check syndicator in UI
|
|
27
29
|
*/
|
|
28
30
|
constructor(options = {}) {
|
|
@@ -92,6 +94,7 @@ export default class BlueskySyndicator {
|
|
|
92
94
|
serviceUrl: this.#serviceUrl,
|
|
93
95
|
includePermalink: this.options.includePermalink,
|
|
94
96
|
syndicateExternalLikes: this.options.syndicateExternalLikes,
|
|
97
|
+
syndicateExternalReposts: this.options.syndicateExternalReposts,
|
|
95
98
|
});
|
|
96
99
|
|
|
97
100
|
return await bluesky.post(properties, publication.me);
|
package/lib/bluesky.js
CHANGED
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
getPostImage,
|
|
7
7
|
getPostText,
|
|
8
8
|
getLikePostText,
|
|
9
|
+
getRepostPostText,
|
|
10
|
+
getBookmarkPostText,
|
|
11
|
+
buildPostText,
|
|
9
12
|
getPostParts,
|
|
10
13
|
uriToPostUrl,
|
|
11
14
|
fetchOpenGraphData,
|
|
@@ -22,6 +25,7 @@ export class Bluesky {
|
|
|
22
25
|
* @param {string} options.serviceUrl - Service URL
|
|
23
26
|
* @param {boolean} [options.includePermalink] - Include permalink in status
|
|
24
27
|
* @param {boolean} [options.syndicateExternalLikes] - Syndicate likes of external URLs
|
|
28
|
+
* @param {boolean} [options.syndicateExternalReposts] - Syndicate reposts of external URLs
|
|
25
29
|
*/
|
|
26
30
|
constructor(options) {
|
|
27
31
|
this.identifier = options.identifier;
|
|
@@ -30,6 +34,7 @@ export class Bluesky {
|
|
|
30
34
|
this.serviceUrl = options.serviceUrl;
|
|
31
35
|
this.includePermalink = options.includePermalink || false;
|
|
32
36
|
this.syndicateExternalLikes = options.syndicateExternalLikes !== false; // Default true
|
|
37
|
+
this.syndicateExternalReposts = options.syndicateExternalReposts !== false; // Default true
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
/**
|
|
@@ -330,7 +335,15 @@ export class Bluesky {
|
|
|
330
335
|
if (isSameOrigin(repostUrl, this.profileUrl)) {
|
|
331
336
|
return this.postRepost(repostUrl);
|
|
332
337
|
}
|
|
333
|
-
|
|
338
|
+
|
|
339
|
+
// Syndicate reposts of external URLs as posts with link card
|
|
340
|
+
if (this.syndicateExternalReposts) {
|
|
341
|
+
const text = getRepostPostText(properties, repostUrl);
|
|
342
|
+
const richText = await createRichText(client, text);
|
|
343
|
+
const externalEmbed = await this.createExternalEmbed(repostUrl);
|
|
344
|
+
return this.postPost(richText, { images, externalEmbed });
|
|
345
|
+
}
|
|
346
|
+
|
|
334
347
|
return;
|
|
335
348
|
}
|
|
336
349
|
|
|
@@ -355,26 +368,28 @@ export class Bluesky {
|
|
|
355
368
|
return;
|
|
356
369
|
}
|
|
357
370
|
|
|
358
|
-
// Handle bookmarks -
|
|
371
|
+
// Handle bookmarks - OG card shows bookmarked URL, text has commentary + permalink
|
|
359
372
|
const bookmarkOfUrl = properties["bookmark-of"];
|
|
360
373
|
if (bookmarkOfUrl) {
|
|
361
|
-
const text =
|
|
374
|
+
const text = getBookmarkPostText(properties, bookmarkOfUrl);
|
|
362
375
|
const richText = await createRichText(client, text);
|
|
363
|
-
// Create external embed for the bookmarked URL
|
|
364
376
|
const externalEmbed = await this.createExternalEmbed(bookmarkOfUrl);
|
|
365
377
|
return this.postPost(richText, { images, externalEmbed });
|
|
366
378
|
}
|
|
367
379
|
|
|
368
|
-
// Regular post -
|
|
369
|
-
const
|
|
380
|
+
// Regular post - determine external URL and build text accordingly
|
|
381
|
+
const externalUrl = getExternalUrl(properties);
|
|
382
|
+
const text = buildPostText(properties, { externalUrl });
|
|
370
383
|
const richText = await createRichText(client, text);
|
|
371
384
|
|
|
372
|
-
//
|
|
385
|
+
// Create OG embed:
|
|
386
|
+
// - External URL exists → use it as OG card (permalink is in text)
|
|
387
|
+
// - No external URL → use permalink as OG card
|
|
373
388
|
let externalEmbed = null;
|
|
374
389
|
if (!images?.length) {
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
377
|
-
externalEmbed = await this.createExternalEmbed(
|
|
390
|
+
const embedUrl = externalUrl || properties.url;
|
|
391
|
+
if (embedUrl) {
|
|
392
|
+
externalEmbed = await this.createExternalEmbed(embedUrl);
|
|
378
393
|
}
|
|
379
394
|
}
|
|
380
395
|
|
package/lib/utils.js
CHANGED
|
@@ -180,7 +180,7 @@ export const uriToPostUrl = (profileUrl, uri) => {
|
|
|
180
180
|
};
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Get post text from given JF2 properties
|
|
183
|
+
* Get post text from given JF2 properties (legacy, used for likes/bookmarks)
|
|
184
184
|
* @param {object} properties - JF2 properties
|
|
185
185
|
* @param {boolean} [includePermalink] - Include permalink in post
|
|
186
186
|
* @returns {string} Post text
|
|
@@ -210,7 +210,92 @@ export const getPostText = (properties, includePermalink) => {
|
|
|
210
210
|
};
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
*
|
|
213
|
+
* Extract plain text content from properties without appending URLs
|
|
214
|
+
* @param {object} properties - JF2 properties
|
|
215
|
+
* @returns {string} Plain text content
|
|
216
|
+
*/
|
|
217
|
+
export function getContentText(properties) {
|
|
218
|
+
if (properties.name && properties.name !== "") {
|
|
219
|
+
return properties.name;
|
|
220
|
+
}
|
|
221
|
+
if (properties.content?.html) {
|
|
222
|
+
return htmlToText(properties.content.html, {
|
|
223
|
+
selectors: [
|
|
224
|
+
{ selector: "a", options: { ignoreHref: true } },
|
|
225
|
+
{ selector: "img", format: "skip" },
|
|
226
|
+
],
|
|
227
|
+
wordwrap: false,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (properties.content?.text) {
|
|
231
|
+
return properties.content.text;
|
|
232
|
+
}
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Remove a URL from text, cleaning up surrounding whitespace and prefixes
|
|
238
|
+
* @param {string} text - Text containing the URL
|
|
239
|
+
* @param {string} url - URL to remove
|
|
240
|
+
* @returns {string} Cleaned text
|
|
241
|
+
*/
|
|
242
|
+
export function removeUrlFromText(text, url) {
|
|
243
|
+
if (!text || !url) return text;
|
|
244
|
+
let result = text.replace(url, "");
|
|
245
|
+
// Clean up common prefixes left behind when URL was at the end
|
|
246
|
+
result = result.replace(/\s*(Réf|Ref|ref|via|Via|source|Source|link|Link|→|—)\s*$/im, "");
|
|
247
|
+
// Clean up excessive whitespace
|
|
248
|
+
result = result.replace(/\n{3,}/g, "\n\n").replace(/[ \t]{2,}/g, " ").trim();
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Build post text for Bluesky syndication with proper URL and character handling.
|
|
254
|
+
*
|
|
255
|
+
* Two modes:
|
|
256
|
+
* 1. External URL exists → remove from text (shown as OG card), append permalink as text
|
|
257
|
+
* 2. No external URL → content only (permalink shown as OG card)
|
|
258
|
+
*
|
|
259
|
+
* @param {object} properties - JF2 properties
|
|
260
|
+
* @param {object} [options] - Options
|
|
261
|
+
* @param {string} [options.externalUrl] - External URL that will be used as OG card
|
|
262
|
+
* @returns {string} Post text fitting within 300 chars
|
|
263
|
+
*/
|
|
264
|
+
export function buildPostText(properties, options = {}) {
|
|
265
|
+
const { externalUrl } = options;
|
|
266
|
+
const LIMIT = 300;
|
|
267
|
+
|
|
268
|
+
let text = getContentText(properties);
|
|
269
|
+
|
|
270
|
+
if (externalUrl) {
|
|
271
|
+
// External URL will be shown as OG card — remove from text to save space
|
|
272
|
+
text = removeUrlFromText(text, externalUrl);
|
|
273
|
+
|
|
274
|
+
// Always append permalink to original post
|
|
275
|
+
const permalink = properties.url;
|
|
276
|
+
if (permalink) {
|
|
277
|
+
const suffix = `\n\n${permalink}`;
|
|
278
|
+
const available = LIMIT - suffix.length;
|
|
279
|
+
if (text.length > available) {
|
|
280
|
+
text = text.slice(0, available - 1).trim() + "\u2026";
|
|
281
|
+
}
|
|
282
|
+
return (text + suffix).trim();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// No external URL — permalink will be shown as OG card
|
|
287
|
+
// No need to duplicate it in text
|
|
288
|
+
if (text.length > LIMIT) {
|
|
289
|
+
text = text.slice(0, LIMIT - 1).trim() + "\u2026";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return text.trim();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get post text for a like of an external URL (Bluesky version).
|
|
297
|
+
* The external URL is shown as an OG card embed, so the text contains
|
|
298
|
+
* commentary + blog permalink (for webmentions), not the liked URL.
|
|
214
299
|
* @param {object} properties - JF2 properties
|
|
215
300
|
* @param {string} likedUrl - The URL being liked
|
|
216
301
|
* @returns {string} Post text
|
|
@@ -225,20 +310,125 @@ export const getLikePostText = (properties, likedUrl) => {
|
|
|
225
310
|
text = properties.content.text;
|
|
226
311
|
}
|
|
227
312
|
|
|
228
|
-
//
|
|
229
|
-
if (text) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
313
|
+
// Remove the liked URL from text if already present (OG card shows it)
|
|
314
|
+
if (text && likedUrl) {
|
|
315
|
+
text = text.replace(likedUrl, "").trim();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Append blog permalink for webmentions
|
|
319
|
+
const permalink = properties.url;
|
|
320
|
+
if (permalink) {
|
|
321
|
+
if (text) {
|
|
322
|
+
text = `${text}\n\n${permalink}`;
|
|
323
|
+
} else {
|
|
324
|
+
text = `❤️ ${permalink}`;
|
|
233
325
|
}
|
|
234
|
-
} else {
|
|
235
|
-
//
|
|
326
|
+
} else if (!text) {
|
|
327
|
+
// Fallback: no content, no permalink — use liked URL
|
|
236
328
|
text = `❤️ ${likedUrl}`;
|
|
237
329
|
}
|
|
238
330
|
|
|
239
331
|
// Truncate if needed (Bluesky limit is 300 chars)
|
|
240
332
|
if (text.length > 300) {
|
|
241
|
-
const suffix = `\n\n❤️ ${likedUrl}`;
|
|
333
|
+
const suffix = permalink ? `\n\n${permalink}` : `\n\n❤️ ${likedUrl}`;
|
|
334
|
+
const maxLen = 300 - suffix.length - 3;
|
|
335
|
+
const contentPart = text.replace(suffix, "").slice(0, maxLen).trim();
|
|
336
|
+
text = contentPart + "..." + suffix;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return text;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get post text for a repost of an external URL (Bluesky version).
|
|
344
|
+
* The external URL is shown as an OG card embed, so the text contains
|
|
345
|
+
* commentary + blog permalink (for webmentions), not the reposted URL.
|
|
346
|
+
* @param {object} properties - JF2 properties
|
|
347
|
+
* @param {string} repostUrl - The URL being reposted
|
|
348
|
+
* @returns {string} Post text
|
|
349
|
+
*/
|
|
350
|
+
export const getRepostPostText = (properties, repostUrl) => {
|
|
351
|
+
let text = "";
|
|
352
|
+
|
|
353
|
+
// Get the content/comment
|
|
354
|
+
if (properties.content?.html) {
|
|
355
|
+
text = htmlToStatusText(properties.content.html);
|
|
356
|
+
} else if (properties.content?.text) {
|
|
357
|
+
text = properties.content.text;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Remove the reposted URL from text if already present (OG card shows it)
|
|
361
|
+
if (text && repostUrl) {
|
|
362
|
+
text = text.replace(repostUrl, "").trim();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Append blog permalink for webmentions
|
|
366
|
+
const permalink = properties.url;
|
|
367
|
+
if (permalink) {
|
|
368
|
+
if (text) {
|
|
369
|
+
text = `${text}\n\n${permalink}`;
|
|
370
|
+
} else {
|
|
371
|
+
text = `🔁 ${permalink}`;
|
|
372
|
+
}
|
|
373
|
+
} else if (!text) {
|
|
374
|
+
// Fallback: no content, no permalink — use repost URL
|
|
375
|
+
text = `🔁 ${repostUrl}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Truncate if needed (Bluesky limit is 300 chars)
|
|
379
|
+
if (text.length > 300) {
|
|
380
|
+
const suffix = permalink ? `\n\n${permalink}` : `\n\n🔁 ${repostUrl}`;
|
|
381
|
+
const maxLen = 300 - suffix.length - 3;
|
|
382
|
+
const contentPart = text.replace(suffix, "").slice(0, maxLen).trim();
|
|
383
|
+
text = contentPart + "..." + suffix;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return text;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get post text for a bookmark of an external URL (Bluesky version).
|
|
391
|
+
* The external URL is shown as an OG card embed, so the text contains
|
|
392
|
+
* commentary + blog permalink (for webmentions), not the bookmarked URL.
|
|
393
|
+
* @param {object} properties - JF2 properties
|
|
394
|
+
* @param {string} bookmarkUrl - The URL being bookmarked
|
|
395
|
+
* @returns {string} Post text
|
|
396
|
+
*/
|
|
397
|
+
export const getBookmarkPostText = (properties, bookmarkUrl) => {
|
|
398
|
+
let text = "";
|
|
399
|
+
|
|
400
|
+
// Get the content/comment
|
|
401
|
+
if (properties.name && properties.name !== "") {
|
|
402
|
+
text = properties.name;
|
|
403
|
+
} else if (properties.content?.html) {
|
|
404
|
+
text = htmlToStatusText(properties.content.html);
|
|
405
|
+
} else if (properties.content?.text) {
|
|
406
|
+
text = properties.content.text;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Remove the bookmarked URL from text if already present (OG card shows it)
|
|
410
|
+
if (text && bookmarkUrl) {
|
|
411
|
+
text = text.replace(bookmarkUrl, "").trim();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Append blog permalink for webmentions
|
|
415
|
+
const permalink = properties.url;
|
|
416
|
+
if (permalink) {
|
|
417
|
+
if (text) {
|
|
418
|
+
if (!text.includes(permalink)) {
|
|
419
|
+
text = `${text}\n\n${permalink}`;
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
text = `🔖 ${permalink}`;
|
|
423
|
+
}
|
|
424
|
+
} else if (!text) {
|
|
425
|
+
// Fallback: no content, no permalink — use bookmarked URL
|
|
426
|
+
text = `🔖 ${bookmarkUrl}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Truncate if needed (Bluesky limit is 300 chars)
|
|
430
|
+
if (text.length > 300) {
|
|
431
|
+
const suffix = permalink ? `\n\n${permalink}` : `\n\n🔖 ${bookmarkUrl}`;
|
|
242
432
|
const maxLen = 300 - suffix.length - 3;
|
|
243
433
|
const contentPart = text.replace(suffix, "").slice(0, maxLen).trim();
|
|
244
434
|
text = contentPart + "..." + suffix;
|