← 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 };