Add automatic Threads link replies and sunrise preview card
This commit is contained in:
@@ -7,7 +7,8 @@ test('default post shows sunrise time rather than the five-minute-later capture
|
||||
const config = loadCaptions(new URL('../.threads-captions.sample.json', import.meta.url));
|
||||
const result = renderCaption(config, { weather: { ...sample, cloudCover: 90 }, capturedAt: '2026-11-01T21:58:00Z',
|
||||
sunrise: '2026-11-01T21:53:00Z', url: 'https://sunrise.horoli.kr', test: false });
|
||||
assert.equal(result.text, '오늘의 관악산 일출이에요\n2026.11.02 · 일출 06:53\n날씨: 흐림\nsunrise.horoli.kr');
|
||||
assert.equal(result.text, '오늘의 관악산 일출이에요\n2026.11.02 · 일출 06:53\n날씨: 흐림');
|
||||
assert.equal(result.replyText, '관악산 일출 보러 가기\nhttps://sunrise.horoli.kr');
|
||||
});
|
||||
test('caption weather uses precipitation first and does not assume missing data means clear', () => {
|
||||
const config = loadCaptions();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { replyToPublished } from '../server/threads/reply.ts';
|
||||
|
||||
test('confirmed reply is recorded separately and skipped on restart', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'threads-reply-'));
|
||||
const path = join(dir, 'record.json');
|
||||
let calls = 0;
|
||||
try {
|
||||
const original = JSON.stringify({ status: 'published', postId: 'parent', imagesDeletedAt: 'now',
|
||||
replyText: 'Link', replyUrl: 'https://example.com' });
|
||||
await writeFile(path, original);
|
||||
const publish = async (_token: string, post: string, text: string, url: string, claim: (id: string) => Promise<void>) => {
|
||||
calls++;
|
||||
assert.equal(post, 'parent');
|
||||
assert.equal(text, 'Link');
|
||||
assert.equal(url, 'https://example.com');
|
||||
await claim('container');
|
||||
return 'reply-id';
|
||||
};
|
||||
await replyToPublished(path, async () => 'token', publish);
|
||||
await replyToPublished(path, async () => 'token', publish);
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(await readFile(path, 'utf8'), original);
|
||||
assert.equal(JSON.parse(await readFile(`${path}.reply.json`, 'utf8')).replyId, 'reply-id');
|
||||
} finally { await rm(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('reply waits for image cleanup, and a lost publish response is never sent twice', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'threads-reply-'));
|
||||
const path = join(dir, 'record.json');
|
||||
const record = { status: 'published', postId: 'parent', replyText: 'Link', replyUrl: 'https://example.com' };
|
||||
let calls = 0;
|
||||
const publish = async (_token: string, _post: string, _text: string, _url: string, claim: (id: string) => Promise<void>) => {
|
||||
calls++;
|
||||
await claim('container');
|
||||
throw new Error('Connection lost');
|
||||
};
|
||||
try {
|
||||
await writeFile(path, JSON.stringify(record));
|
||||
await replyToPublished(path, async () => 'token', publish);
|
||||
assert.equal(calls, 0);
|
||||
await writeFile(path, JSON.stringify({ ...record, imagesDeletedAt: 'now' }));
|
||||
await assert.rejects(replyToPublished(path, async () => 'token', publish), /Connection lost/);
|
||||
await replyToPublished(path, async () => 'token', publish);
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(JSON.parse(await readFile(`${path}.reply.json`, 'utf8')).status, 'publishing');
|
||||
} finally { await rm(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
+19
-1
@@ -2,7 +2,25 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { calculateSun } from '../src/astronomy/sunCalculator.ts';
|
||||
import { observer } from '../src/config/observer.ts';
|
||||
import { dailySchedule, publishImage, publishCarousel } from '../server/threads/publisher.ts';
|
||||
import { dailySchedule, publishImage, publishCarousel, publishReply } from '../server/threads/publisher.ts';
|
||||
|
||||
test('reply targets the published parent and attaches a preview link, claiming before publish', async () => {
|
||||
const replies = [{ id: 'reply-container' }, { status: 'FINISHED' }, { id: 'reply' }];
|
||||
let claimed = false;
|
||||
const id = await publishReply('token', 'parent-post', 'Visit us', 'https://sunrise.horoli.kr', async () => {
|
||||
claimed = true;
|
||||
}, async (url, init) => {
|
||||
const params = init?.body as URLSearchParams;
|
||||
if (String(url).endsWith('/me/threads')) {
|
||||
assert.equal(params.get('reply_to_id'), 'parent-post');
|
||||
assert.equal(params.get('link_attachment'), 'https://sunrise.horoli.kr');
|
||||
assert.equal(params.get('media_type'), 'TEXT');
|
||||
}
|
||||
if (String(url).endsWith('/me/threads_publish')) assert.ok(claimed);
|
||||
return new Response(JSON.stringify(replies.shift()));
|
||||
}, async () => {});
|
||||
assert.equal(id, 'reply');
|
||||
});
|
||||
|
||||
test('schedule uses the Korean date and existing SunCalc sunrise', () => {
|
||||
const now = new Date('2026-11-01T18:00:00Z');
|
||||
|
||||
Reference in New Issue
Block a user