Add sunrise Threads automation and improve capture and character interactions
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
import { calculateSun } from '../../src/astronomy/sunCalculator.ts';
|
||||
import { observer } from '../../src/config/observer.ts';
|
||||
import { kstFields } from '../../src/ui/time.ts';
|
||||
import { THREADS } from '../../src/constants/threads.ts';
|
||||
|
||||
export function dailySchedule(now: Date) {
|
||||
const sunrise = calculateSun(now, observer).sunrise;
|
||||
if (!sunrise || !Number.isFinite(+sunrise)) throw new Error('No sunrise for this date');
|
||||
return { date: kstFields(now).date, sunrise, prepareAt: new Date(+sunrise - THREADS.prepareMs) };
|
||||
}
|
||||
|
||||
export async function threadsRequest(token: string, path: string, params: Record<string, string>, method = 'GET', request: typeof fetch = fetch) {
|
||||
const url = new URL(path, THREADS.apiUrl);
|
||||
if (method === 'GET') url.search = new URLSearchParams(params).toString();
|
||||
const response = await request(url, {
|
||||
method, headers: { Authorization: `Bearer ${token}` },
|
||||
body: method === 'POST' ? new URLSearchParams(params) : undefined,
|
||||
signal: AbortSignal.timeout(THREADS.requestTimeoutMs),
|
||||
});
|
||||
const data = await response.json();
|
||||
// Do not log response bodies: authentication errors may echo credentials.
|
||||
if (!response.ok || data.error) throw new Error(`Threads API failed (HTTP ${response.status}, code ${data.error?.code ?? 'unknown'})`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function publishImage(token: string, imageUrl: string, text: string,
|
||||
beforePublish: (containerId: string) => Promise<void>, request: typeof fetch = fetch,
|
||||
wait: (ms: number) => Promise<unknown> = delay) {
|
||||
const container = await threadsRequest(token, '/me/threads', {
|
||||
media_type: 'IMAGE', image_url: imageUrl, text,
|
||||
alt_text: '관악산 정상에서 바라본 일출 시각의 3D 시뮬레이션',
|
||||
}, 'POST', request);
|
||||
if (!container.id) throw new Error('Threads returned no container ID');
|
||||
return publishContainer(token, container.id, beforePublish, request, wait);
|
||||
}
|
||||
|
||||
export async function publishCarousel(token: string, imageUrls: string[], text: string,
|
||||
beforePublish: (containerId: string) => Promise<void>, request: typeof fetch = fetch,
|
||||
wait: (ms: number) => Promise<unknown> = delay) {
|
||||
if (imageUrls.length !== THREADS.angles.length) throw new Error('Both sunrise angles are required');
|
||||
const children: string[] = [];
|
||||
for (const [index, imageUrl] of imageUrls.entries()) {
|
||||
const child = await threadsRequest(token, '/me/threads', {
|
||||
media_type: 'IMAGE', image_url: imageUrl, is_carousel_item: 'true',
|
||||
alt_text: `${THREADS.angles[index].alt} · 3D 시뮬레이션`,
|
||||
}, 'POST', request);
|
||||
if (!child.id) throw new Error('Threads returned no child container ID');
|
||||
await waitForMedia(token, child.id, request, wait);
|
||||
children.push(child.id);
|
||||
}
|
||||
const container = await threadsRequest(token, '/me/threads', {
|
||||
media_type: 'CAROUSEL', children: children.join(','), text,
|
||||
}, 'POST', request);
|
||||
if (!container.id) throw new Error('Threads returned no carousel ID');
|
||||
return publishContainer(token, container.id, beforePublish, request, wait);
|
||||
}
|
||||
|
||||
async function publishContainer(token: string, id: string, beforePublish: (id: string) => Promise<void>,
|
||||
request: typeof fetch, wait: (ms: number) => Promise<unknown>) {
|
||||
await waitForMedia(token, id, request, wait);
|
||||
await beforePublish(id);
|
||||
const post = await threadsRequest(token, '/me/threads_publish', { creation_id: id }, 'POST', request);
|
||||
if (!post.id) throw new Error('Threads returned no post ID; check account before retrying');
|
||||
return String(post.id);
|
||||
}
|
||||
|
||||
async function waitForMedia(token: string, id: string, request: typeof fetch, wait: (ms: number) => Promise<unknown>) {
|
||||
for (let i = 0; i < THREADS.mediaPollAttempts; i++) {
|
||||
const media = await threadsRequest(token, `/${id}`, { fields: 'status,error_message' }, 'GET', request);
|
||||
if (media.status === 'FINISHED') return;
|
||||
if (['ERROR', 'EXPIRED', 'PUBLISHED'].includes(media.status)) throw new Error(`Threads container status: ${media.status}`);
|
||||
await wait(THREADS.mediaPollMs);
|
||||
}
|
||||
throw new Error('Threads media processing timed out');
|
||||
}
|
||||
Reference in New Issue
Block a user