Skip to content

Instantly share code, notes, and snippets.

@nschwermann
Created September 20, 2025 18:15
Show Gist options
  • Save nschwermann/dbb6fdc2f89674c6141b30225a526175 to your computer and use it in GitHub Desktop.
Save nschwermann/dbb6fdc2f89674c6141b30225a526175 to your computer and use it in GitHub Desktop.
/**
* Twitter API v2 - Post a Tweet with GIF Example
*
* This example demonstrates how to post a tweet with a GIF attachment
* using the twitter-api-v2 library and Twitter's v2 API endpoints.
*
* Prerequisites:
* 1. Twitter Developer Account
* 2. App with Read and Write permissions
* 3. API Keys and Access Tokens
* 4. npm install twitter-api-v2 dotenv
*/
import { TwitterApi } from 'twitter-api-v2';
import fs from 'fs/promises';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// Initialize Twitter client with v1.1 and v2 access
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,
});
// Get read/write client
const rwClient = client.readWrite;
/**
* Upload a GIF to Twitter and get media ID
* @param {string} gifPath - Path to the GIF file
* @returns {Promise<string>} Media ID for the uploaded GIF
*/
async function uploadGif(gifPath) {
try {
console.log(`πŸ“€ Uploading GIF from: ${gifPath}`);
// Read the GIF file as a buffer
const gifBuffer = await fs.readFile(gifPath);
// Get file stats for size validation
const stats = await fs.stat(gifPath);
const fileSizeMB = stats.size / (1024 * 1024);
// Twitter's GIF size limit is 15MB
if (fileSizeMB > 15) {
throw new Error(`GIF size (${fileSizeMB.toFixed(2)}MB) exceeds Twitter's 15MB limit`);
}
console.log(`πŸ“Š GIF size: ${fileSizeMB.toFixed(2)}MB`);
// Upload the GIF using v1.1 media upload endpoint
// Note: v1.1 endpoint is still required for media uploads
const mediaId = await client.v1.uploadMedia(gifBuffer, {
mimeType: 'image/gif',
// Optional: Add media category for better processing
additionalOwners: undefined,
mediaCategory: 'tweet_gif'
});
console.log(`βœ… GIF uploaded successfully! Media ID: ${mediaId}`);
return mediaId;
} catch (error) {
console.error('❌ Error uploading GIF:', error.message);
throw error;
}
}
/**
* Post a tweet with GIF attachment
* @param {string} tweetText - The text content of the tweet
* @param {string} gifPath - Path to the GIF file
* @returns {Promise<object>} Tweet response object
*/
async function postTweetWithGif(tweetText, gifPath) {
try {
// Step 1: Upload the GIF and get media ID
const mediaId = await uploadGif(gifPath);
// Step 2: Post the tweet with the media attachment using v2 endpoint
console.log('πŸ“ Posting tweet with GIF...');
const tweet = await rwClient.v2.tweet({
text: tweetText,
media: {
media_ids: [mediaId]
}
});
console.log('βœ… Tweet posted successfully!');
console.log(`πŸ”— Tweet ID: ${tweet.data.id}`);
console.log(`πŸ“± View tweet: https://twitter.com/i/web/status/${tweet.data.id}`);
return tweet.data;
} catch (error) {
console.error('❌ Error posting tweet:', error.message);
// Handle specific Twitter API errors
if (error.code === 'ENOENT') {
console.error('File not found. Please check the GIF path.');
} else if (error.data?.errors) {
error.data.errors.forEach(err => {
console.error(`Twitter API Error: ${err.message}`);
});
}
throw error;
}
}
/**
* Verify Twitter connection and permissions
* @returns {Promise<boolean>} True if connection is successful
*/
async function verifyConnection() {
try {
const user = await rwClient.v2.me();
console.log(`βœ… Connected as @${user.data.username}`);
console.log(`πŸ‘€ Name: ${user.data.name}`);
return true;
} catch (error) {
console.error('❌ Failed to connect to Twitter:', error.message);
return false;
}
}
/**
* Main execution function
*/
async function main() {
console.log('🐦 Twitter GIF Posting Example');
console.log('================================\n');
// Verify connection first
const connected = await verifyConnection();
if (!connected) {
console.error('Please check your API credentials in .env file');
process.exit(1);
}
console.log('\n');
// Example usage
const tweetText = "πŸš€ Check out this amazing animated GIF! #TwitterAPI #NodeJS";
const gifPath = "./output/animation.gif"; // Path to your GIF file
try {
const tweet = await postTweetWithGif(tweetText, gifPath);
console.log('\n✨ Success! Your tweet with GIF has been posted.');
console.log('Tweet data:', JSON.stringify(tweet, null, 2));
} catch (error) {
console.error('\nπŸ’₯ Failed to post tweet');
process.exit(1);
}
}
// Execute if run directly
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(console.error);
}
// Export functions for use as a module
export { uploadGif, postTweetWithGif, verifyConnection };
/**
* Environment Variables (.env file):
*
* TWITTER_API_KEY=your_api_key_here
* TWITTER_API_SECRET=your_api_secret_here
* TWITTER_ACCESS_TOKEN=your_access_token_here
* TWITTER_ACCESS_TOKEN_SECRET=your_access_token_secret_here
*
* Important Notes:
*
* 1. GIF Requirements:
* - Maximum file size: 15MB
* - Supported formats: GIF
* - Recommended dimensions: 1280x720 or smaller
* - Frame rate: 30fps or less for best results
*
* 2. API Rate Limits:
* - Media upload: 415 requests per 15 minutes (app auth)
* - Tweet posting: 200 requests per 15 minutes (user auth)
*
* 3. Permissions:
* - Your Twitter app must have "Read and Write" permissions
* - Set in Twitter Developer Portal > App Settings > User authentication settings
*
* 4. Error Handling:
* - Always validate GIF size before upload
* - Implement retry logic for transient failures
* - Log errors for debugging
*
* 5. Best Practices:
* - Optimize GIF file size using tools like ffmpeg
* - Use appropriate error messages for user feedback
* - Store media IDs if posting multiple times
* - Clean up local files after successful upload
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment