Publish to Bluesky and Threads

medium15 minsocial-publisher.ts

You want to post the same announcement to Bluesky and Threads, now or on a schedule, without posting it twice after a crash and without renewing tokens by hand.

What it shows

  • One FeedPublisher sends a post to every feed and tracks each delivery on its own
  • Text longer than a feed takes is cut at a word for that feed: 300 graphemes on Bluesky, 500 characters on Threads
  • The same key publishes once, and publishAt schedules a post in a store that survives restarts
  • A post quotes another with quote: its id belongs to one feed, so the quote goes to that feed only
  • The Bluesky session and the Threads token live in a TokenStore, and the Threads token renews itself before it expires

The code

social-publisher.ts
import { BlueskyFeed, FeedPublisher, FilePublishStore, FileTokenStore, ThreadsFeed } from '@cogitator-ai/channels';

const tokens = new FileTokenStore();

const publisher = new FeedPublisher({
  feeds: [
    new BlueskyFeed({
      identifier: process.env.BLUESKY_HANDLE!,
      appPassword: process.env.BLUESKY_APP_PASSWORD!,
      store: tokens,
    }),
    new ThreadsFeed({ accessToken: process.env.THREADS_ACCESS_TOKEN!, store: tokens }),
  ],
  store: new FilePublishStore(),
  dryRun: process.env.PUBLISH !== '1',
  onPublished: ({ feed, post }) => console.log(`${feed}: ${post.url || post.id}`),
  onFailed: ({ feed, error, final }) =>
    console.log(`${feed}: ${final ? 'failed' : 'will retry'}: ${error.message}`),
});

const today = new Date().toISOString().slice(0, 10);

const { job, duplicate } = await publisher.publish(
  {
    text: 'Open models now run on an ordinary laptop. What changed, and what it means for you:',
    link: { url: 'https://example.com/story', title: 'Open models on laptops' },
    tags: ['AI'],
    langs: ['en'],
  },
  { key: `story-${today}` }
);
console.log(duplicate ? 'Already published today' : `Job ${job.id}`);

for (const delivery of job.deliveries) {
  if (!delivery.post) continue;
  await publisher.publish(
    { text: 'In case you missed it:', quote: delivery.post.id },
    { feeds: [delivery.feed], key: `quote-${delivery.feed}-${today}` }
  );
}

await publisher.publish(
  { text: 'Good morning! Three things worth reading today:', link: { url: 'https://example.com/brief' } },
  { publishAt: Date.now() + 60_000, key: `brief-${today}` }
);

publisher.start();

process.on('SIGINT', async () => {
  await publisher.stop();
  process.exit(0);
});

Run it

install · bash
pnpm add @cogitator-ai/channels @atproto/api
EnvironmentBLUESKY_HANDLEBLUESKY_APP_PASSWORDTHREADS_ACCESS_TOKENPUBLISH (optional, 1 posts for real)
run · bash
npx tsx social-publisher.ts

Or run the full example from a clone of the repository (keys in .env):

repository · bash
npx tsx examples/channels/04-social-publisher.ts

Learn more