agent-twitter-summary
Version:
A twitter client for agents
596 lines (510 loc) • 20 kB
text/typescript
import { getScraper } from './test-utils';
import { QueryTweetsResponse } from './timeline-v1';
import { Mention, Tweet } from './tweets';
import fs from 'fs';
import path from 'path';
let shouldSkipV2Tests = false;
beforeAll(() => {
const {
TWITTER_API_KEY,
TWITTER_API_SECRET_KEY,
TWITTER_ACCESS_TOKEN,
TWITTER_ACCESS_TOKEN_SECRET,
} = process.env;
if (
!TWITTER_API_KEY ||
!TWITTER_API_SECRET_KEY ||
!TWITTER_ACCESS_TOKEN ||
!TWITTER_ACCESS_TOKEN_SECRET
) {
console.warn(
'Skipping tests: Twitter API v2 keys are not available in environment variables.',
);
shouldSkipV2Tests = true;
}
});
test('scraper can get tweet', async () => {
const expected: Tweet = {
conversationId: '1585338303800578049',
html: `We’re updating Twitter’s sounds to help make them pleasing to more people, including those with sensory sensitivities. Here’s more on how we did it:<br><a href=\"https://blog.twitter.com/en_us/topics/product/2022/designing-accessible-sounds-story-behind-our-new-chirps\">https://t.co/7FKWk7NzHM</a>`,
id: '1585338303800578049',
hashtags: [],
mentions: [],
name: 'A11y',
permanentUrl: 'https://twitter.com/XA11y/status/1585338303800578049',
photos: [],
text: 'We’re updating Twitter’s sounds to help make them pleasing to more people, including those with sensory sensitivities. Here’s more on how we did it:\nhttps://t.co/7FKWk7NzHM',
thread: [],
timeParsed: new Date(Date.UTC(2022, 9, 26, 18, 31, 20, 0)),
timestamp: 1666809080,
urls: [
'https://blog.twitter.com/en_us/topics/product/2022/designing-accessible-sounds-story-behind-our-new-chirps',
],
userId: '1631299117',
username: 'XA11y',
videos: [],
isQuoted: false,
isReply: false,
isRetweet: false,
isPin: false,
sensitiveContent: false,
};
const scraper = await getScraper();
const actual = await scraper.getTweet('1585338303800578049');
delete actual?.likes;
delete actual?.replies;
delete actual?.retweets;
delete actual?.views;
delete actual?.bookmarkCount;
expect(actual).toEqual(expected);
});
test('scraper can get tweets without logging in', async () => {
const scraper = await getScraper({ authMethod: 'anonymous' });
let counter = 0;
for await (const tweet of scraper.getTweets('elonmusk', 10)) {
if (tweet) {
counter++;
}
}
expect(counter).toBeGreaterThanOrEqual(1);
});
test('scraper can get tweets from list', async () => {
const scraper = await getScraper();
let cursor: string | undefined = undefined;
const maxTweets = 30;
let nTweets = 0;
while (nTweets < maxTweets) {
const res: QueryTweetsResponse = await scraper.fetchListTweets(
'1736495155002106192',
maxTweets,
cursor,
);
expect(res.next).toBeTruthy();
nTweets += res.tweets.length;
cursor = res.next;
}
});
test('scraper can get first tweet matching query', async () => {
const scraper = await getScraper();
const timeline = scraper.getTweets('elonmusk');
const latestQuote = await scraper.getTweetWhere(timeline, { isQuoted: true });
expect(latestQuote?.isQuoted).toBeTruthy();
});
test('scraper can get all tweets matching query', async () => {
const scraper = await getScraper();
// Sample size of 20 should be enough without taking long.
const timeline = scraper.getTweets('elonmusk', 20);
const retweets = await scraper.getTweetsWhere(
timeline,
(tweet) => tweet.isRetweet === true,
);
expect(retweets).toBeTruthy();
for (const tweet of retweets) {
expect(tweet.isRetweet).toBe(true);
}
}, 20000);
test('scraper can get latest tweet', async () => {
const scraper = await getScraper();
// OLD APPROACH (without retweet filtering)
const tweets = scraper.getTweets('elonmusk', 1);
const expected = (await tweets.next()).value;
// NEW APPROACH
const latest = (await scraper.getLatestTweet(
'elonmusk',
expected?.isRetweet || false,
)) as Tweet;
expect(latest?.permanentUrl).toEqual(expected?.permanentUrl);
}, 30000);
test('scraper can get user mentions in tweets', async () => {
const expected: Mention[] = [
{
id: '7018222',
username: 'davidmcraney',
name: 'David McRaney',
},
];
const scraper = await getScraper();
const tweet = await scraper.getTweet('1554522888904101890');
expect(tweet?.mentions).toEqual(expected);
});
test('scraper can get tweet quotes without logging in', async () => {
const expected: Tweet = {
conversationId: '1237110546383724547',
html: `The Easiest Problem Everyone Gets Wrong <br><br>[new video] --> <a href=\"https://youtu.be/ytfCdqWhmdg\">https://t.co/YdaeDYmPAU</a> <br><a href=\"https://t.co/iKu4Xs6o2V\"><img src=\"https://pbs.twimg.com/media/ESsZa9AXgAIAYnF.jpg\"/></a>`,
id: '1237110546383724547',
hashtags: [],
mentions: [],
name: 'Vsauce2',
permanentUrl: 'https://twitter.com/VsauceTwo/status/1237110546383724547',
photos: [
{
id: '1237110473486729218',
url: 'https://pbs.twimg.com/media/ESsZa9AXgAIAYnF.jpg',
alt_text: undefined,
},
],
text: 'The Easiest Problem Everyone Gets Wrong \n\n[new video] --> https://t.co/YdaeDYmPAU https://t.co/iKu4Xs6o2V',
thread: [],
timeParsed: new Date(Date.UTC(2020, 2, 9, 20, 18, 33, 0)),
timestamp: 1583785113,
urls: ['https://youtu.be/ytfCdqWhmdg'],
userId: '978944851',
username: 'VsauceTwo',
videos: [],
isQuoted: false,
isReply: false,
isRetweet: false,
isPin: false,
sensitiveContent: false,
};
const scraper = await getScraper({ authMethod: 'anonymous' });
const quote = await scraper.getTweet('1237110897597976576');
expect(quote?.isQuoted).toBeTruthy();
delete quote?.quotedStatus?.likes;
delete quote?.quotedStatus?.replies;
delete quote?.quotedStatus?.retweets;
delete quote?.quotedStatus?.views;
delete quote?.quotedStatus?.bookmarkCount;
expect(quote?.quotedStatus).toEqual(expected);
});
test('scraper can get tweet quotes and replies', async () => {
const expected: Tweet = {
conversationId: '1237110546383724547',
html: `The Easiest Problem Everyone Gets Wrong <br><br>[new video] --> <a href=\"https://youtu.be/ytfCdqWhmdg\">https://t.co/YdaeDYmPAU</a> <br><a href=\"https://t.co/iKu4Xs6o2V\"><img src=\"https://pbs.twimg.com/media/ESsZa9AXgAIAYnF.jpg\"/></a>`,
id: '1237110546383724547',
hashtags: [],
mentions: [],
name: 'Vsauce2',
permanentUrl: 'https://twitter.com/VsauceTwo/status/1237110546383724547',
photos: [
{
id: '1237110473486729218',
url: 'https://pbs.twimg.com/media/ESsZa9AXgAIAYnF.jpg',
alt_text: undefined,
},
],
text: 'The Easiest Problem Everyone Gets Wrong \n\n[new video] --> https://t.co/YdaeDYmPAU https://t.co/iKu4Xs6o2V',
thread: [],
timeParsed: new Date(Date.UTC(2020, 2, 9, 20, 18, 33, 0)),
timestamp: 1583785113,
urls: ['https://youtu.be/ytfCdqWhmdg'],
userId: '978944851',
username: 'VsauceTwo',
videos: [],
isQuoted: false,
isReply: false,
isRetweet: false,
isPin: false,
sensitiveContent: false,
};
const scraper = await getScraper();
const quote = await scraper.getTweet('1237110897597976576');
expect(quote?.isQuoted).toBeTruthy();
delete quote?.quotedStatus?.likes;
delete quote?.quotedStatus?.replies;
delete quote?.quotedStatus?.retweets;
delete quote?.quotedStatus?.views;
delete quote?.quotedStatus?.bookmarkCount;
expect(quote?.quotedStatus).toEqual(expected);
const reply = await scraper.getTweet('1237111868445134850');
expect(reply?.isReply).toBeTruthy();
if (reply != null) {
reply.isReply = false;
}
delete reply?.inReplyToStatus?.likes;
delete reply?.inReplyToStatus?.replies;
delete reply?.inReplyToStatus?.retweets;
delete reply?.inReplyToStatus?.views;
delete reply?.inReplyToStatus?.bookmarkCount;
expect(reply?.inReplyToStatus).toEqual(expected);
});
test('scraper can get retweet', async () => {
const expected: Tweet = {
conversationId: '1776276954435481937',
html: `<br><a href=\"https://t.co/qqiu5ntffp\"><img src=\"https://pbs.twimg.com/amplify_video_thumb/1776276900580622336/img/UknAtyWSZ286nCD3.jpg\"/></a>`,
id: '1776276954435481937',
hashtags: [],
mentions: [],
name: 'federico.',
permanentUrl: 'https://twitter.com/federicosmos/status/1776276954435481937',
photos: [],
text: 'https://t.co/qqiu5ntffp',
thread: [],
timeParsed: new Date(Date.UTC(2024, 3, 5, 15, 53, 22, 0)),
timestamp: 1712332402,
urls: [],
userId: '2376017065',
username: 'federicosmos',
videos: [
{
id: '1776276900580622336',
preview:
'https://pbs.twimg.com/amplify_video_thumb/1776276900580622336/img/UknAtyWSZ286nCD3.jpg',
url: 'https://video.twimg.com/amplify_video/1776276900580622336/vid/avc1/640x360/uACt_egp_hmvPOZF.mp4?tag=14',
},
],
isQuoted: false,
isReply: false,
isRetweet: false,
isPin: false,
sensitiveContent: false,
};
const scraper = await getScraper();
const retweet = await scraper.getTweet('1776285549566808397');
expect(retweet?.isRetweet).toBeTruthy();
delete retweet?.retweetedStatus?.likes;
delete retweet?.retweetedStatus?.replies;
delete retweet?.retweetedStatus?.retweets;
delete retweet?.retweetedStatus?.views;
delete retweet?.retweetedStatus?.bookmarkCount;
expect(retweet?.retweetedStatus).toEqual(expected);
});
test('scraper can get tweet views', async () => {
const expected: Tweet = {
conversationId: '1606055187348688896',
html: `Replies and likes don’t tell the whole story. We’re making it easier to tell *just* how many people have seen your Tweets with the addition of view counts, shown right next to likes. Now on iOS and Android, web coming soon.<br><br><a href=\"https://help.twitter.com/using-twitter/view-counts\">https://t.co/hrlMQyXJfx</a>`,
id: '1606055187348688896',
hashtags: [],
mentions: [],
name: 'Support',
permanentUrl: 'https://twitter.com/Support/status/1606055187348688896',
photos: [],
text: 'Replies and likes don’t tell the whole story. We’re making it easier to tell *just* how many people have seen your Tweets with the addition of view counts, shown right next to likes. Now on iOS and Android, web coming soon.\n\nhttps://t.co/hrlMQyXJfx',
thread: [],
timeParsed: new Date(Date.UTC(2022, 11, 22, 22, 32, 50, 0)),
timestamp: 1671748370,
urls: ['https://help.twitter.com/using-twitter/view-counts'],
userId: '17874544',
username: 'Support',
videos: [],
isQuoted: false,
isReply: false,
isRetweet: false,
isPin: false,
sensitiveContent: false,
};
const scraper = await getScraper();
const actual = await scraper.getTweet('1606055187348688896');
expect(actual?.views).toBeTruthy();
delete actual?.likes;
delete actual?.replies;
delete actual?.retweets;
delete actual?.views;
delete actual?.bookmarkCount;
expect(actual).toEqual(expected);
});
test('scraper can get tweet thread', async () => {
const scraper = await getScraper();
const tweet = await scraper.getTweet('1665602315745673217');
expect(tweet).not.toBeNull();
expect(tweet?.isSelfThread).toBeTruthy();
expect(tweet?.thread.length).toStrictEqual(7);
});
test('scraper can get user tweets', async () => {
const scraper = await getScraper();
const userId = '1830340867737178112'; // Replace with a valid user ID
const maxTweets = 200;
const response = await scraper.getUserTweets(userId, maxTweets);
expect(response.tweets).toBeDefined();
expect(response.tweets.length).toBeLessThanOrEqual(maxTweets);
// Check if each object in the tweets array is a valid Tweet object
response.tweets.forEach((tweet) => {
expect(tweet.id).toBeDefined();
expect(tweet.text).toBeDefined();
});
expect(response.next).toBeDefined();
}, 30000);
test('sendTweet successfully sends a tweet', async () => {
const scraper = await getScraper();
const draftText = 'Core updated on ' + Date.now().toString();
const result = await scraper.sendTweet(draftText);
console.log('Send tweet result:', result);
const replyResult = await scraper.sendTweet(
'Ignore this',
'1430277451452751874',
);
console.log('Send reply result:', replyResult);
});
test('scraper can get a tweet with getTweetV2', async () => {
const scraper = await getScraper({ authMethod: 'api' });
if (shouldSkipV2Tests) {
return console.warn("Skipping 'getTweetV2' test due to missing API keys.");
}
const tweetId = '1856441982811529619';
const tweet: Tweet | null = await scraper.getTweetV2(tweetId);
expect(tweet).not.toBeNull();
expect(tweet?.id).toEqual(tweetId);
expect(tweet?.text).toBeDefined();
});
test('scraper can get multiple tweets with getTweetsV2', async () => {
if (shouldSkipV2Tests) {
return console.warn("Skipping 'getTweetV2' test due to missing API keys.");
}
const scraper = await getScraper({ authMethod: 'api' });
const tweetIds = ['1856441982811529619', '1856429655215260130'];
const tweets = await scraper.getTweetsV2(tweetIds);
expect(tweets).toBeDefined();
expect(tweets.length).toBeGreaterThan(0);
tweets.forEach((tweet) => {
expect(tweet.id).toBeDefined();
expect(tweet.text).toBeDefined();
});
});
test('scraper can send a tweet with sendTweetV2', async () => {
if (shouldSkipV2Tests) {
return console.warn("Skipping 'getTweetV2' test due to missing API keys.");
}
const scraper = await getScraper({ authMethod: 'api' });
const tweetText = `Automated test tweet at ${Date.now()}`;
const response = await scraper.sendTweetV2(tweetText);
expect(response).not.toBeNull();
expect(response?.id).toBeDefined();
expect(response?.text).toEqual(tweetText);
});
test('scraper can create a poll with sendTweetV2', async () => {
if (shouldSkipV2Tests) {
return console.warn("Skipping 'getTweetV2' test due to missing API keys.");
}
const scraper = await getScraper({ authMethod: 'api' });
const tweetText = `When do you think we'll achieve AGI (Artificial General Intelligence)? 🤖 Cast your prediction!`;
const pollData = {
poll: {
options: [
{ label: '2025 🗓️' },
{ label: '2026 📅' },
{ label: '2027 🛠️' },
{ label: '2030+ 🚀' },
],
duration_minutes: 1440,
},
};
const response = await scraper.sendTweetV2(tweetText, undefined, pollData);
expect(response).not.toBeNull();
expect(response?.id).toBeDefined();
expect(response?.text).toEqual(tweetText);
// Validate poll structure in response
const poll = response?.poll;
expect(poll).toBeDefined();
expect(poll?.options.map((option) => option.label)).toEqual(
pollData?.poll.options.map((option) => option.label),
);
});
test('scraper can send a tweet without media', async () => {
const scraper = await getScraper();
const draftText = 'Test tweet without media ' + Date.now().toString();
// Send a tweet without any media attachments
const result = await scraper.sendTweet(draftText);
// Log and verify the result
console.log('Send tweet without media result:', result);
expect(result.ok).toBeTruthy();
}, 30000);
test('scraper can send a tweet with image and video', async () => {
const scraper = await getScraper();
const draftText = 'Test tweet with image and video ' + Date.now().toString();
// Read test image and video files from the test-assets directory
const imageBuffer = fs.readFileSync(
path.join(__dirname, '../test-assets/test-image.jpeg')
);
const videoBuffer = fs.readFileSync(
path.join(__dirname, '../test-assets/test-video.mp4')
);
// Prepare media data array with both image and video
const mediaData = [
{ data: imageBuffer, mediaType: 'image/jpeg' },
{ data: videoBuffer, mediaType: 'video/mp4' },
];
// Send a tweet with both image and video attachments
const result = await scraper.sendTweet(draftText, undefined, mediaData);
// Log and verify the result
console.log('Send tweet with image and video result:', result);
expect(result.ok).toBeTruthy();
}, 30000);
test('scraper can quote tweet without media', async () => {
const scraper = await getScraper();
const quotedTweetId = '1776276954435481937';
const quoteText = `Testing quote tweet without media ${Date.now()}`;
// Send a quote tweet without any media attachments
const response = await scraper.sendQuoteTweet(quoteText, quotedTweetId);
// Log and verify the response
console.log('Quote tweet without media result:', response);
expect(response.ok).toBeTruthy();
}, 30000);
test('scraper can quote tweet with image and video', async () => {
const scraper = await getScraper();
const quotedTweetId = '1776276954435481937';
const quoteText = `Testing quote tweet with image and video ${Date.now()}`;
// Read test image and video files from the test-assets directory
const imageBuffer = fs.readFileSync(
path.join(__dirname, '../test-assets/test-image.jpeg')
);
const videoBuffer = fs.readFileSync(
path.join(__dirname, '../test-assets/test-video.mp4')
);
// Prepare media data array with both image and video
const mediaData = [
{ data: imageBuffer, mediaType: 'image/jpeg' },
{ data: videoBuffer, mediaType: 'video/mp4' },
];
// Send a quote tweet with both image and video attachments
const response = await scraper.sendQuoteTweet(quoteText, quotedTweetId, {
mediaData: mediaData,
});
// Log and verify the response
console.log('Quote tweet with image and video result:', response);
expect(response.ok).toBeTruthy();
}, 30000);
test('scraper can quote tweet with media', async () => {
const scraper = await getScraper();
const quotedTweetId = '1776276954435481937';
const quoteText = `Testing quote tweet with media ${Date.now()}`;
// Read test image file
const imageBuffer = fs.readFileSync(
path.join(__dirname, '../test-assets/test-image.jpeg')
);
// Prepare media data with the image
const mediaData = [
{ data: imageBuffer, mediaType: 'image/jpeg' },
];
// Send a quote tweet with the image attachment
const response = await scraper.sendQuoteTweet(quoteText, quotedTweetId, {
mediaData: mediaData,
});
// Log and verify the response
console.log('Quote tweet with media result:', response);
expect(response.ok).toBeTruthy();
}, 30000);
test('sendTweetWithMedia successfully sends a tweet with media', async () => {
const scraper = await getScraper();
const draftText = 'Test tweet with media ' + Date.now().toString();
// Read a test image file
const imageBuffer = fs.readFileSync(
path.join(__dirname, '../test-assets/test-image.jpeg')
);
// Prepare media data with the image
const mediaData = [
{ data: imageBuffer, mediaType: 'image/jpeg' },
];
// Send a tweet with the image attachment
const result = await scraper.sendTweet(draftText, undefined, mediaData);
// Log and verify the result
console.log('Send tweet with media result:', result);
expect(result.ok).toBeTruthy();
}, 30000);
test('scraper can like a tweet', async () => {
const scraper = await getScraper();
const tweetId = '1776276954435481937'; // Use a real tweet ID for testing
// Test should not throw an error
await expect(scraper.likeTweet(tweetId)).resolves.not.toThrow();
});
test('scraper can retweet', async () => {
const scraper = await getScraper();
const tweetId = '1776276954435481937';
// Test should not throw an error
await expect(scraper.retweet(tweetId)).resolves.not.toThrow();
});
test('scraper can follow user', async () => {
const scraper = await getScraper();
const username = 'elonmusk'; // Use a real username for testing
// Test should not throw an error
await expect(scraper.followUser(username)).resolves.not.toThrow();
}, 30000);