Add Sweetistics API posting support to bird

This commit is contained in:
Peter Steinberger
2025-12-02 12:36:54 +00:00
parent d8703625a3
commit 850d59e68d
4 changed files with 199 additions and 2 deletions
+65 -2
View File
@@ -13,6 +13,7 @@ import { Command } from 'commander';
import { resolveCredentials } from './lib/cookies.js';
import { extractTweetId } from './lib/extract-tweet-id.js';
import { TwitterClient } from './lib/twitter-client.js';
import { SweetisticsClient } from './lib/sweetistics-client.js';
const program = new Command();
@@ -22,7 +23,21 @@ program.name('bird').description('Post tweets and replies via Twitter/X GraphQL
program
.option('--auth-token <token>', 'Twitter auth_token cookie')
.option('--ct0 <token>', 'Twitter ct0 cookie')
.option('--chrome-profile <name>', 'Chrome profile name for cookie extraction');
.option('--chrome-profile <name>', 'Chrome profile name for cookie extraction')
.option('--sweetistics-api-key <key>', 'Sweetistics API key (or set SWEETISTICS_API_KEY)')
.option('--sweetistics-base-url <url>', 'Sweetistics base URL', process.env.SWEETISTICS_BASE_URL || 'https://sweetistics.com');
function resolveSweetisticsConfig(options: { sweetisticsApiKey?: string; sweetisticsBaseUrl?: string }) {
const apiKey =
options.sweetisticsApiKey ||
process.env.SWEETISTICS_API_KEY ||
process.env.SWEETISTICS_LOCALHOST_API_KEY ||
null;
const baseUrl = options.sweetisticsBaseUrl || process.env.SWEETISTICS_BASE_URL || 'https://sweetistics.com';
return { apiKey, baseUrl };
}
// Tweet command
program
@@ -31,6 +46,30 @@ program
.argument('<text>', 'Tweet text')
.action(async (text: string) => {
const opts = program.opts();
const sweetistics = resolveSweetisticsConfig(opts);
if (sweetistics.apiKey) {
try {
const client = new SweetisticsClient({
baseUrl: sweetistics.baseUrl,
apiKey: sweetistics.apiKey,
});
const result = await client.tweet(text);
if (result.success) {
console.log('✅ Tweet posted via Sweetistics!');
if (result.tweetId) {
console.log(`🔗 https://x.com/i/status/${result.tweetId}`);
}
return;
}
console.error(`❌ Sweetistics post failed: ${result.error ?? 'Unknown error'}`);
process.exit(1);
} catch (error) {
console.error(`❌ Sweetistics error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
const { cookies, warnings } = await resolveCredentials({
authToken: opts.authToken,
ct0: opts.ct0,
@@ -70,6 +109,31 @@ program
.argument('<text>', 'Reply text')
.action(async (tweetIdOrUrl: string, text: string) => {
const opts = program.opts();
const sweetistics = resolveSweetisticsConfig(opts);
const tweetId = extractTweetId(tweetIdOrUrl);
if (sweetistics.apiKey) {
try {
const client = new SweetisticsClient({
baseUrl: sweetistics.baseUrl,
apiKey: sweetistics.apiKey,
});
const result = await client.tweet(text, tweetId);
if (result.success) {
console.log('✅ Reply posted via Sweetistics!');
if (result.tweetId) {
console.log(`🔗 https://x.com/i/status/${result.tweetId}`);
}
return;
}
console.error(`❌ Sweetistics reply failed: ${result.error ?? 'Unknown error'}`);
process.exit(1);
} catch (error) {
console.error(`❌ Sweetistics error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
const { cookies, warnings } = await resolveCredentials({
authToken: opts.authToken,
ct0: opts.ct0,
@@ -89,7 +153,6 @@ program
console.error(`📍 Using credentials from: ${cookies.source}`);
}
const tweetId = extractTweetId(tweetIdOrUrl);
console.error(`📝 Replying to tweet: ${tweetId}`);
const client = new TwitterClient({ cookies });
+70
View File
@@ -0,0 +1,70 @@
export interface SweetisticsClientOptions {
baseUrl: string;
apiKey: string;
userAgent?: string;
}
export interface SweetisticsTweetResult {
success: boolean;
tweetId?: string;
error?: string;
}
function normalizeBaseUrl(value: string): string {
const trimmed = value.trim();
if (!trimmed) return 'https://sweetistics.com';
return trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed;
}
export class SweetisticsClient {
private readonly baseUrl: string;
private readonly apiKey: string;
private readonly userAgent?: string;
constructor(options: SweetisticsClientOptions) {
this.baseUrl = normalizeBaseUrl(options.baseUrl);
this.apiKey = options.apiKey.trim();
this.userAgent = options.userAgent;
if (!this.apiKey) {
throw new Error('Sweetistics API key is required');
}
}
async tweet(text: string, replyToTweetId?: string): Promise<SweetisticsTweetResult> {
const payload: Record<string, unknown> = { text };
if (replyToTweetId) {
payload.replyToTweetId = replyToTweetId;
}
const response = await fetch(`${this.baseUrl}/api/actions/tweet`, {
method: 'POST',
headers: {
authorization: `Bearer ${this.apiKey}`,
'content-type': 'application/json',
...(this.userAgent ? { 'user-agent': this.userAgent } : {}),
},
body: JSON.stringify(payload),
});
let data: unknown;
try {
data = await response.json();
} catch (error) {
return {
success: false,
error: `Sweetistics response parse failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
const success = typeof (data as { success?: unknown })?.success === 'boolean' ? (data as { success: boolean }).success : false;
const tweetId = typeof (data as { tweetId?: unknown })?.tweetId === 'string' ? (data as { tweetId?: string }).tweetId : undefined;
const errorMessage = typeof (data as { error?: unknown })?.error === 'string' ? (data as { error?: string }).error : undefined;
if (!response.ok || !success) {
const reason = errorMessage || `HTTP ${response.status}`;
return { success: false, error: reason };
}
return { success: true, tweetId };
}
}