Skip to content

Commit

Permalink
Merge pull request #3 from Sifchain/pollCreation
Browse files Browse the repository at this point in the history
[Twitter] Added Generalizable V2 API with examples starting with Polls
  • Loading branch information
jkbrooks authored Nov 15, 2024
2 parents f9b6dc3 + 99e3c04 commit 7ba97d2
Show file tree
Hide file tree
Showing 10 changed files with 802 additions and 29 deletions.
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# for v1 api support
TWITTER_USERNAME=myaccount
TWITTER_PASSWORD=MyPassword!!!
TWITTER_EMAIL=[email protected]
PROXY_URL= # HTTP(s) proxy for requests (optional)

# for v2 api support
TWITTER_API_KEY=key
TWITTER_API_SECRET_KEY=secret
TWITTER_ACCESS_TOKEN=token
TWITTER_ACCESS_TOKEN_SECRET=tokensecret

# optional
PROXY_URL= # HTTP(s) proxy for requests
96 changes: 82 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,115 @@
# agent-twitter-client

This is a modified version of [@the-convocation/twitter-scraper](https://github.com/the-convocation/twitter-scraper) with added functionality for sending tweets and retweets. This package does not require Twitter API to use, and will run in both the browser and server.
This is a modified version of [@the-convocation/twitter-scraper](https://github.com/the-convocation/twitter-scraper) with added functionality for sending tweets and retweets. This package does not require the Twitter API to use and will run in both the browser and server.

## Installation

```sh
npm install agent-twitter-client
```

## Setup

Configure environment variables for authentication.

```
TWITTER_USERNAME= # Account username
TWITTER_PASSWORD= # Account password
TWITTER_EMAIL= # Account email
PROXY_URL= # HTTP(s) proxy for requests (necessary for browsers)
# Twitter API v2 credentials for tweet and poll functionality
TWITTER_API_KEY= # Twitter API Key
TWITTER_API_SECRET_KEY= # Twitter API Secret Key
TWITTER_ACCESS_TOKEN= # Access Token for Twitter API v2
TWITTER_ACCESS_TOKEN_SECRET= # Access Token Secret for Twitter API v2
```

### Getting Twitter Cookies
It is important that you use Twitter cookies so that you don't send a new login request to twitter every time you want to do something.

In your application, you will probably want to have a check for cookies. If you don't have cookies, log in with user auth credentials. Then, cache the cookies for future use.
It is important to use Twitter cookies to avoid sending a new login request to Twitter every time you want to perform an action.

In your application, you will likely want to check for existing cookies. If cookies are not available, log in with user authentication credentials and cache the cookies for future use.

```ts
const scraper = await getScraper({ authMethod: 'password' });
const scraper = await getScraper({ authMethod: 'password' });

scraper.getCookies().then((cookies) => {
console.log(cookies);
// Remove 'Cookies' and save the cookies as a JSON array
});
scraper.getCookies().then((cookies) => {
console.log(cookies);
// Remove 'Cookies' and save the cookies as a JSON array
});
```

## Getting Started

```ts
const scraper = new Scraper();
await scraper.login('username', 'password');

// If using v2 functionality (currently required to support polls)
await scraper.login(
'username',
'password',
'email',
'appKey',
'appSecret',
'accessToken',
'accessSecret',
);

const tweets = await scraper.getTweets('elonmusk', 10);
const tweetsAndReplies = scraper.getTweetsAndReplies('elonmusk');
const latestTweet = await scraper.getLatestTweet('elonmusk');
const tweet = await scraper.getTweet('1234567890123456789');
await scraper.sendTweet('Hello world!');

// Create a poll
await scraper.sendTweetV2(
`What's got you most hyped? Let us know! 🤖💸`,
undefined,
{
poll: {
options: [
{ label: 'AI Innovations 🤖' },
{ label: 'Crypto Craze 💸' },
{ label: 'Both! 🌌' },
{ label: 'Neither for Me 😅' },
],
durationMinutes: 120, // Duration of the poll in minutes
},
},
);
```

### Fetching Specific Tweet Data (V2)

```ts
// Fetch a single tweet with poll details
const tweet = await scraper.getTweetV2('1856441982811529619', {
expansions: ['attachments.poll_ids'],
pollFields: ['options', 'end_datetime'],
});
console.log('tweet', tweet);

// Fetch multiple tweets with poll and media details
const tweets = await scraper.getTweetsV2(
['1856441982811529619', '1856429655215260130'],
{
expansions: ['attachments.poll_ids', 'attachments.media_keys'],
pollFields: ['options', 'end_datetime'],
mediaFields: ['url', 'preview_image_url'],
},
);
console.log('tweets', tweets);
```

## API

### Authentication

```ts
// Log in
await scraper.login('username', 'password');
await scraper.login('username', 'password');

// Log out
await scraper.logout();
Expand All @@ -65,23 +128,25 @@ await scraper.clearCookies();
```

### Profile

```ts
// Get a user's profile
const profile = await scraper.getProfile('TwitterDev');

// Get a user ID from their screen name
// Get a user ID from their screen name
const userId = await scraper.getUserIdByScreenName('TwitterDev');
```

### Search

```ts
import { SearchMode } from 'agent-twitter-client';

// Search for recent tweets
const tweets = scraper.searchTweets('#nodejs', 20, SearchMode.Latest);

// Search for profiles
const profiles = scraper.searchProfiles('John', 10);
const profiles = scraper.searchProfiles('John', 10);

// Fetch a page of tweet results
const results = await scraper.fetchSearchTweets('#nodejs', 20, SearchMode.Top);
Expand All @@ -91,6 +156,7 @@ const profileResults = await scraper.fetchSearchProfiles('John', 10);
```

### Relationships

```ts
// Get a user's followers
const followers = scraper.getFollowers('12345', 100);
Expand All @@ -101,11 +167,12 @@ const following = scraper.getFollowing('12345', 100);
// Fetch a page of a user's followers
const followerResults = await scraper.fetchProfileFollowers('12345', 100);

// Fetch a page of who a user is following
// Fetch a page of who a user is following
const followingResults = await scraper.fetchProfileFollowing('12345', 100);
```

### Trends

```ts
// Get current trends
const trends = await scraper.getTrends();
Expand All @@ -115,6 +182,7 @@ const listTweets = await scraper.fetchListTweets('1234567890', 50);
```

### Tweets

```ts
// Get a user's tweets
const tweets = scraper.getTweets('TwitterDev');
Expand All @@ -129,12 +197,12 @@ const tweetsAndReplies = scraper.getTweetsAndReplies('TwitterDev');
const timeline = scraper.getTweets('TwitterDev', 100);
const retweets = await scraper.getTweetsWhere(
timeline,
(tweet) => tweet.isRetweet
(tweet) => tweet.isRetweet,
);

// Get a user's latest tweet
const latestTweet = await scraper.getLatestTweet('TwitterDev');

// Get a specific tweet by ID
const tweet = await scraper.getTweet('1234567890123456789');
```
```
51 changes: 51 additions & 0 deletions SampleAgent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Scraper } from 'agent-twitter-client';
import dotenv from 'dotenv';
dotenv.config();

async function main() {
// const scraper = new Scraper();
// // v1 login
// await scraper.login(
// process.env.TWITTER_USERNAME,
// process.env.TWITTER_PASSWORD,
// );
// // v2 login
// await scraper.login(
// process.env.TWITTER_USERNAME,
// process.env.TWITTER_PASSWORD,
// undefined,
// undefined,
// process.env.TWITTER_API_KEY,
// process.env.TWITTER_API_SECRET_KEY,
// process.env.TWITTER_ACCESS_TOKEN,
// process.env.TWITTER_ACCESS_TOKEN_SECRET,
// );
// console.log('Logged in successfully!');
// // Example: Posting a new tweet with a poll
// await scraper.sendTweetV2(
// `When do you think we'll achieve AGI (Artificial General Intelligence)? 🤖 Cast your prediction!`,
// undefined,
// {
// poll: {
// options: [
// { label: '2025 🗓️' },
// { label: '2026 📅' },
// { label: '2027 🛠️' },
// { label: '2030+ 🚀' },
// ],
// durationMinutes: 1440,
// },
// },
// );
// console.log(await scraper.getTweet('1856441982811529619'));
// const tweet = await scraper.getTweetV2('1856441982811529619');
// console.log({ tweet });
// console.log('tweet', tweet);
// const tweets = await scraper.getTweetsV2([
// '1856441982811529619',
// '1856429655215260130',
// ]);
// console.log('tweets', tweets);
}

main();
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
},
"dependencies": {
"@sinclair/typebox": "^0.32.20",
"agent-twitter-client": "^0.0.13",
"headers-polyfill": "^3.1.2",
"json-stable-stringify": "^1.0.2",
"otpauth": "^9.2.2",
"set-cookie-parser": "^2.6.0",
"tough-cookie": "^4.1.2",
"tslib": "^2.5.2"
"tslib": "^2.5.2",
"twitter-api-v2": "^1.18.2"
},
"devDependencies": {
"@commitlint/cli": "^17.6.3",
Expand All @@ -50,14 +52,14 @@
"@types/tough-cookie": "^4.0.2",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"dotenv": "^16.3.1",
"dotenv": "^16.4.5",
"esbuild": "^0.21.5",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"gh-pages": "^5.0.0",
"https-proxy-agent": "^7.0.2",
"jest": "^29.5.0",
"jest": "^29.7.0",
"lint-staged": "^13.2.2",
"prettier": "^2.8.8",
"rimraf": "^5.0.7",
Expand Down
7 changes: 7 additions & 0 deletions src/auth-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ export class TwitterUserAuth extends TwitterGuestAuth {
password: string,
email?: string,
twoFactorSecret?: string,
appKey?: string,
appSecret?: string,
accessToken?: string,
accessSecret?: string,
): Promise<void> {
await this.updateGuestToken();

Expand Down Expand Up @@ -111,6 +115,9 @@ export class TwitterUserAuth extends TwitterGuestAuth {
throw new Error(`Unknown subtask ${next.subtask.subtask_id}`);
}
}
if (appKey && appSecret && accessToken && accessSecret) {
this.loginWithV2(appKey, appSecret, accessToken, accessSecret);
}
if ('err' in next) {
throw next.err;
}
Expand Down
37 changes: 37 additions & 0 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Cookie, CookieJar, MemoryCookieStore } from 'tough-cookie';
import { updateCookieJar } from './requests';
import { Headers } from 'headers-polyfill';
import { FetchTransformOptions } from './api';
import { TwitterApi } from 'twitter-api-v2';

export interface TwitterAuthOptions {
fetch: typeof fetch;
Expand All @@ -16,6 +17,21 @@ export interface TwitterAuth {
*/
cookieJar(): CookieJar;

/**
* Logs into a Twitter account using the v2 API
*/
loginWithV2(
appKey: string,
appSecret: string,
accessToken: string,
accessSecret: string,
): void;

/**
* Get v2 API client if it exists
*/
getV2Client(): TwitterApi | null;

/**
* Returns if a user is logged-in to Twitter through this instance.
* @returns `true` if a user is logged-in; otherwise `false`.
Expand Down Expand Up @@ -94,6 +110,7 @@ export class TwitterGuestAuth implements TwitterAuth {
protected jar: CookieJar;
protected guestToken?: string;
protected guestCreatedAt?: Date;
protected v2Client: TwitterApi | null;

fetch: typeof fetch;

Expand All @@ -104,12 +121,32 @@ export class TwitterGuestAuth implements TwitterAuth {
this.fetch = withTransform(options?.fetch ?? fetch, options?.transform);
this.bearerToken = bearerToken;
this.jar = new CookieJar();
this.v2Client = null;
}

cookieJar(): CookieJar {
return this.jar;
}

getV2Client(): TwitterApi | null {
return this.v2Client ?? null;
}

loginWithV2(
appKey: string,
appSecret: string,
accessToken: string,
accessSecret: string,
): void {
const v2Client = new TwitterApi({
appKey,
appSecret,
accessToken,
accessSecret,
});
this.v2Client = v2Client;
}

isLoggedIn(): Promise<boolean> {
return Promise.resolve(false);
}
Expand Down
Loading

0 comments on commit 7ba97d2

Please sign in to comment.