From 54885ac3fd0b88883cb986531555a67de6f515e1 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sun, 15 Jun 2025 21:41:39 +0200 Subject: [PATCH] action: fix thread - Fix the splitting of the text, previously it included the separator line - Fix the population of the reply record, previously the later replies (from the third) won't have the correct record updated and won't be posted. --- actions/lib/posts.js | 52 +++++++++++++++++-- actions/login-and-validate.js | 14 ++--- actions/process.js | 7 +-- actions/test/examples/new/thread.json.example | 5 ++ actions/test/examples/new/thread.txt | 9 ++++ actions/test/integration.js | 13 +++++ 6 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 actions/test/examples/new/thread.json.example create mode 100644 actions/test/examples/new/thread.txt diff --git a/actions/lib/posts.js b/actions/lib/posts.js index 6ce7cbe..28fb8c8 100644 --- a/actions/lib/posts.js +++ b/actions/lib/posts.js @@ -193,12 +193,8 @@ export async function populateRecord(agent, request, shouldUploadImage = false) $type: 'app.bsky.embed.record', record: request.repostInfo }; - } else if (request.replyInfo) { - record.reply = { - root: request.rootInfo || request.replyInfo, - parent: request.replyInfo, - }; } + updateReplyRecord(request, record); // If there is already another embed, don't generate the card embed. if (!record.embed) { @@ -238,6 +234,52 @@ export async function populateRecord(agent, request, shouldUploadImage = false) return request; } +function updateReplyRecord(request, record) { + if (request.replyInfo) { + record.reply = { + root: request.rootInfo || request.replyInfo, + parent: request.replyInfo, + }; + } + return request; +} + +export function maybeUpdateReplyInThread(request, previousPostInfo, rootPostInfo) { + if (request.replyURL === REPLY_IN_THREAD) { + request.replyInfo = previousPostInfo; + request.rootInfo = rootPostInfo; + updateReplyRecord(request, request.record); + console.log('Updated reply in thread', request); + } +} + +// If the request contains rich text with thematic breaks, it will split the request into multiple +// requests. +export function maybeSplitRequests(request) { + if (request.action === 'repost') { // reposts are always single posts. + return [request]; + } + if (!request.richText) { + return [request]; + } + const thread = request.richText.split(/^\s*(?:[-*_]\s*){2,}\s*$/m) + .map((text) => text.trim()) + .filter((text) => text.length > 0); + + if (thread.length === 1) { + return [request]; + } + + return thread.map((richText, i) => ({ + ...request, + ...(i === 0 ? undefined : { + action: 'reply', // Posts other than the first one are replies. + replyURL: REPLY_IN_THREAD, + }), + richText, + })); +} + /** * @param {AtpAgent} agent * @param {object} request diff --git a/actions/login-and-validate.js b/actions/login-and-validate.js index f0170b0..2299624 100755 --- a/actions/login-and-validate.js +++ b/actions/login-and-validate.js @@ -5,7 +5,7 @@ import process from 'node:process'; import path from 'node:path'; import { login } from './lib/login.js'; import { validateAccount, validateRequest } from './lib/validator.js'; -import { populateRecord, REPLY_IN_THREAD } from './lib/posts.js'; +import { populateRecord, maybeSplitRequests } from './lib/posts.js'; // The JSON file must contains the following fields: // - "account": a string field indicating the account to use to perform the action. @@ -21,16 +21,8 @@ if (Object.hasOwn(request, 'richTextFile')) { richTextFile = path.resolve(path.dirname(requestFilePath), request.richTextFile); request.richText = fs.readFileSync(richTextFile, 'utf-8'); } -const threadElements = request.action !== 'repost' && request.richText?.split(/\n+ {0,3}([-_*])[ \t]*(?:\1[ \t]*){2,}\n+/g); -const requests = threadElements?.length ? - threadElements.map((richText, i) => ({ - ...request, - ...(i === 0 ? undefined : { - action: 'reply', - replyURL: REPLY_IN_THREAD, - }), - richText, - })) : [request]; + +const requests = maybeSplitRequests(request); // Validate the account field. const account = validateAccount(request, process.env); diff --git a/actions/process.js b/actions/process.js index 7d59936..cafef2c 100755 --- a/actions/process.js +++ b/actions/process.js @@ -4,7 +4,7 @@ import fs from 'node:fs'; import assert from 'node:assert'; import process from 'node:process'; import path from 'node:path'; -import { post, REPLY_IN_THREAD } from './lib/posts.js'; +import { post, maybeUpdateReplyInThread } from './lib/posts.js'; // This script takes a path to a JSON with the pattern $base_path/new/$any_name.json, // where $any_name can be anything, and then performs the action specified in it. @@ -38,10 +38,7 @@ for (const request of requests) { break; } case 'reply': { - if (request.replyURL === REPLY_IN_THREAD) { - request.replyInfo = previousPostInfo; - request.rootInfo = rootPostInfo; - } + maybeUpdateReplyInThread(request, previousPostInfo, rootPostInfo); console.log(`Replying...`, request.replyURL, request.richText); result = await post(agent, request); break; diff --git a/actions/test/examples/new/thread.json.example b/actions/test/examples/new/thread.json.example new file mode 100644 index 0000000..48d4059 --- /dev/null +++ b/actions/test/examples/new/thread.json.example @@ -0,0 +1,5 @@ +{ + "action": "post", + "account": "PRIMARY", + "richTextFile": "./thread.txt" +} diff --git a/actions/test/examples/new/thread.txt b/actions/test/examples/new/thread.txt new file mode 100644 index 0000000..17c3dbf --- /dev/null +++ b/actions/test/examples/new/thread.txt @@ -0,0 +1,9 @@ +This is thread post one. + +--- + +This is thread post two. + +--- + +This is thread post three. diff --git a/actions/test/integration.js b/actions/test/integration.js index 7c1f454..90d5212 100644 --- a/actions/test/integration.js +++ b/actions/test/integration.js @@ -98,4 +98,17 @@ console.log(repostRequest); checkProcess(process.execPath, [ processPath, repostPath ]); // repost alone does not generate new URLs. +// Test threading. +const threadPath = path.join(newDir, 'thread.json'); +const threadExample = path.join(examplesDir, 'thread.json.example'); +const threadRequest = loadJSON(threadExample); +fs.cpSync(threadExample, threadPath); +fs.cpSync(path.join(examplesDir, 'thread.txt'), path.join(newDir, 'thread.txt')); + +console.log('--- Test threading ---'); +console.log(threadRequest); +const threadChild = checkProcess(process.execPath, [ processPath, threadPath ]); +const threadURL = await getURLFromLastResult(threadChild.stdout); +console.log(`thread URL`, threadURL); + fs.rmSync(tmpdir, { recursive: true, force: true });