import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import * as fs from "fs"; @Injectable() export class TwitterService { private readonly logger = new Logger(TwitterService.name); private client: any = null; private isEnabled = false; constructor(private readonly configService: ConfigService) { const apiKey = this.configService.get("TWITTER_API_KEY"); const apiSecret = this.configService.get("TWITTER_API_SECRET"); const accessToken = this.configService.get("TWITTER_ACCESS_TOKEN"); const accessSecret = this.configService.get( "TWITTER_ACCESS_SECRET", ); if (apiKey && apiSecret && accessToken && accessSecret) { void this.initClient(apiKey, apiSecret, accessToken, accessSecret); } else { this.logger.warn( "⚠️ X/Twitter API keys not configured. Set TWITTER_API_KEY, TWITTER_API_SECRET, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_SECRET", ); } } private async initClient( apiKey: string, apiSecret: string, accessToken: string, accessSecret: string, ) { try { const { TwitterApi } = await import("twitter-api-v2"); this.client = new TwitterApi({ appKey: apiKey, appSecret: apiSecret, accessToken, accessSecret, }); this.isEnabled = true; this.logger.log("✅ Twitter API client initialized"); } catch (error) { this.logger.error("Failed to initialize Twitter client", error); } } get available(): boolean { return this.isEnabled && this.client !== null; } /** * Post a tweet with an image. * * @param text - Tweet text * @param imagePath - Absolute path to the image file * @returns Tweet ID */ async postWithImage(text: string, imagePath: string): Promise { if (!this.available) { this.logger.warn("Twitter not available, skipping post"); return null; } try { // Step 1: Upload image media via the X media upload endpoint. const mediaData = fs.readFileSync(imagePath); const mediaId = await this.client.v1.uploadMedia(mediaData, { mimeType: this.getMimeType(imagePath), }); // Step 2: Create tweet via v2 const tweet = await this.client.v2.tweet({ text, media: { media_ids: [mediaId] }, }); const tweetId = tweet.data?.id; this.logger.log(`✅ Tweet posted: ${tweetId}`); return tweetId || null; } catch (error) { this.logger.error(`❌ Twitter post failed: ${error.message}`); return null; } } private getMimeType(imagePath: string): string { const ext = imagePath.toLowerCase().split(".").pop(); if (ext === "jpg" || ext === "jpeg") return "image/jpeg"; if (ext === "webp") return "image/webp"; if (ext === "png") return "image/png"; return "image/jpeg"; } }