← back to Norma

agents/bluesky-agent/lib/bluesky.js

83 lines

/**
 * Bluesky / AT Protocol utilities
 *
 * Uses @atproto/api for posting to Bluesky.
 * Falls back to simulation mode if credentials aren't set.
 * Credentials loaded from DB first, with env var fallback.
 */

const { loadCredentials } = require('../../shared/credentials');

// Lazy-loaded credentials (DB-first, env-fallback)
let _creds = null;
async function _getCreds() {
  if (!_creds) _creds = await loadCredentials('bluesky');
  return _creds || { BSKY_HANDLE: '', BSKY_PASSWORD: '', BSKY_SERVICE: 'https://bsky.social' };
}
// Refresh cache every 5 min
setInterval(() => { _creds = null; }, 5 * 60 * 1000);

async function hasCredentials() {
  const c = await _getCreds();
  return !!(c.BSKY_HANDLE && c.BSKY_PASSWORD);
}

/**
 * Post to Bluesky.
 * Returns simulated result if no credentials configured.
 *
 * @param {string} text - Post text (max 300 chars for Bluesky)
 * @param {Object} [options]
 * @param {string} [options.url] - URL to include as a link card
 * @returns {Promise<{ id: string|null, url: string|null, simulated: boolean, posted: boolean, error: string|null }>}
 */
async function createPost(text, options = {}) {
  const hasCreds = await hasCredentials();
  const c = await _getCreds();
  if (!hasCreds) {
    console.log(`[bluesky] Simulation mode — would post: "${text.substring(0, 80)}..."`);
    return {
      id: `sim_${Date.now()}`,
      url: null,
      simulated: true,
      posted: false,
      error: null,
      would_post: {
        text: text.substring(0, 300),
        service: c.BSKY_SERVICE,
        handle: c.BSKY_HANDLE || '(not configured)',
      },
    };
  }

  try {
    // Dynamic import for @atproto/api
    const { BskyAgent } = require('@atproto/api');
    const agent = new BskyAgent({ service: c.BSKY_SERVICE });
    await agent.login({ identifier: c.BSKY_HANDLE, password: c.BSKY_PASSWORD });

    const record = { text: text.substring(0, 300), createdAt: new Date().toISOString() };

    const result = await agent.post(record);

    return {
      id: result.uri,
      url: `https://bsky.app/profile/${c.BSKY_HANDLE}/post/${result.uri.split('/').pop()}`,
      simulated: false,
      posted: true,
      error: null,
    };
  } catch (err) {
    console.error('[bluesky] Post failed:', err.message);
    return {
      id: null,
      url: null,
      simulated: false,
      posted: false,
      error: err.message,
    };
  }
}

module.exports = { createPost, hasCredentials };