← back to Goodquestion Ai
scripts/social-reply.cjs
158 lines
#!/usr/bin/env node
/**
* Social Reply — reply to comments on Bluesky + X/Twitter
* Usage:
* node social-reply.cjs --platform bsky --uri "at://..." --cid "baf..." --text "Thanks!"
* node social-reply.cjs --platform x --tweet-id "123456" --text "Thanks!"
* node social-reply.cjs --latest # Show latest reply-worthy notifications
* node social-reply.cjs --latest --auto # Auto-generate and post replies (AI mode)
*/
require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });
const { BskyAgent, RichText } = require('@atproto/api');
const { TwitterApi } = require('twitter-api-v2');
const fs = require('fs');
const BSKY_HANDLE = 'agentabrams.bsky.social';
const BSKY_PASSWORD = process.env.BSKY_PASSWORD;
function parseArgs() {
const args = process.argv.slice(2);
const r = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--platform' && args[i + 1]) r.platform = args[++i];
if (args[i] === '--uri' && args[i + 1]) r.uri = args[++i];
if (args[i] === '--cid' && args[i + 1]) r.cid = args[++i];
if (args[i] === '--tweet-id' && args[i + 1]) r.tweetId = args[++i];
if (args[i] === '--text' && args[i + 1]) r.text = args[++i];
if (args[i] === '--latest') r.latest = true;
if (args[i] === '--auto') r.auto = true;
}
return r;
}
async function replyBluesky(uri, cid, text) {
const agent = new BskyAgent({ service: 'https://bsky.social' });
await agent.login({ identifier: BSKY_HANDLE, password: BSKY_PASSWORD });
// If no CID provided, fetch the post to get it
if (!cid) {
// Extract DID and rkey from URI: at://did:plc:xxx/app.bsky.feed.post/rkey
const parts = uri.split('/');
const did = parts[2];
const rkey = parts[4];
const thread = await agent.getPostThread({ uri });
cid = thread.data.thread.post.cid;
}
// Determine root (for threading)
let root = { uri, cid };
try {
const thread = await agent.getPostThread({ uri });
if (thread.data.thread.post.record.reply) {
root = thread.data.thread.post.record.reply.root;
}
} catch (e) {
// If we can't get thread, treat this post as root
}
const rt = new RichText({ text });
await rt.detectFacets(agent);
const result = await agent.post({
text: rt.text,
facets: rt.facets,
reply: {
root: root,
parent: { uri, cid },
},
createdAt: new Date().toISOString(),
});
const postId = result.uri.split('/').pop();
return {
success: true,
url: `https://bsky.app/profile/${BSKY_HANDLE}/post/${postId}`,
uri: result.uri,
};
}
async function replyTwitter(tweetId, text) {
const client = new TwitterApi({
appKey: process.env.TWITTER_API_KEY,
appSecret: process.env.TWITTER_API_SECRET,
accessToken: process.env.TWITTER_ACCESS_TOKEN,
accessSecret: process.env.TWITTER_ACCESS_TOKEN_SECRET,
});
let replyText = text;
if (replyText.length > 280) replyText = replyText.substring(0, 277) + '...';
const result = await client.v2.reply(replyText, tweetId);
return {
success: true,
id: result.data.id,
url: `https://x.com/agentabrams/status/${result.data.id}`,
};
}
async function showLatest() {
// Load latest monitor results
const dataFile = '/tmp/social-monitor-latest.json';
if (!fs.existsSync(dataFile)) {
console.log('No recent notifications. Run social-monitor.cjs first.');
return [];
}
const items = JSON.parse(fs.readFileSync(dataFile, 'utf-8'));
const replyWorthy = items.filter(n => n.type === 'reply' || n.type === 'mention');
if (replyWorthy.length === 0) {
console.log('No replies or mentions to respond to.');
return [];
}
console.log(`\n${replyWorthy.length} reply-worthy notifications:\n`);
replyWorthy.forEach((n, i) => {
const platform = n.platform === 'bluesky' ? '[BSky]' : '[X]';
console.log(`${i + 1}. ${platform} @${n.author} (${n.type}):`);
console.log(` "${n.text}"`);
if (n.uri) console.log(` Reply: node social-reply.cjs --platform bsky --uri "${n.uri}" --text "your reply"`);
if (n.tweetId) console.log(` Reply: node social-reply.cjs --platform x --tweet-id "${n.tweetId}" --text "your reply"`);
console.log();
});
return replyWorthy;
}
async function main() {
const opts = parseArgs();
if (opts.latest) {
await showLatest();
return;
}
if (!opts.platform || !opts.text) {
console.error('Usage:');
console.error(' node social-reply.cjs --platform bsky --uri "at://..." --text "reply"');
console.error(' node social-reply.cjs --platform x --tweet-id "123" --text "reply"');
console.error(' node social-reply.cjs --latest');
process.exit(1);
}
if (opts.platform === 'bsky') {
if (!opts.uri) { console.error('--uri required for Bluesky'); process.exit(1); }
console.log(`[Bluesky] Replying to ${opts.uri}...`);
const result = await replyBluesky(opts.uri, opts.cid, opts.text);
console.log('[Bluesky] Reply posted:', result.url);
} else if (opts.platform === 'x') {
if (!opts.tweetId) { console.error('--tweet-id required for X'); process.exit(1); }
console.log(`[X] Replying to tweet ${opts.tweetId}...`);
const result = await replyTwitter(opts.tweetId, opts.text);
console.log('[X] Reply posted:', result.url);
}
}
main().catch(e => { console.error('Error:', e.message); process.exit(1); });