@cm-growth-hacking/twitter-client 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/bin/twitter.js +1129 -0
- package/dist/cli/output.d.ts +22 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +71 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +196 -0
- package/dist/client/TwitterClient.d.ts +47 -0
- package/dist/client/TwitterClient.d.ts.map +1 -0
- package/dist/client/TwitterClient.js +472 -0
- package/dist/client/auth.d.ts +21 -0
- package/dist/client/auth.d.ts.map +1 -0
- package/dist/client/auth.js +42 -0
- package/dist/client/graphql.d.ts +49 -0
- package/dist/client/graphql.d.ts.map +1 -0
- package/dist/client/graphql.js +191 -0
- package/dist/client/parsers.d.ts +14 -0
- package/dist/client/parsers.d.ts.map +1 -0
- package/dist/client/parsers.js +147 -0
- package/dist/client/query-ids.d.ts +22 -0
- package/dist/client/query-ids.d.ts.map +1 -0
- package/dist/client/query-ids.js +157 -0
- package/dist/client/rest.d.ts +10 -0
- package/dist/client/rest.d.ts.map +1 -0
- package/dist/client/rest.js +41 -0
- package/dist/client/types.d.ts +80 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Tweet, User, NewsItem } from '../client/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Format a tweet for display
|
|
4
|
+
*/
|
|
5
|
+
export declare function formatTweet(tweet: Tweet, showMetrics?: boolean): string;
|
|
6
|
+
/**
|
|
7
|
+
* Format a user for display
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatUser(user: User): string;
|
|
10
|
+
/**
|
|
11
|
+
* Format a news item for display
|
|
12
|
+
*/
|
|
13
|
+
export declare function formatNewsItem(item: NewsItem, index: number): string;
|
|
14
|
+
/**
|
|
15
|
+
* Format error message
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatError(error: string, code?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Print a separator line
|
|
20
|
+
*/
|
|
21
|
+
export declare function separator(char?: string, length?: number): string;
|
|
22
|
+
//# sourceMappingURL=output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/cli/output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEhE;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,GAAE,OAAc,GAAG,MAAM,CAc7E;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAe7C;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAgBpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,GAAE,MAAY,EAAE,MAAM,GAAE,MAAW,GAAG,MAAM,CAEzE"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a tweet for display
|
|
3
|
+
*/
|
|
4
|
+
export function formatTweet(tweet, showMetrics = true) {
|
|
5
|
+
let output = `@${tweet.author.username} (${tweet.author.name})\n`;
|
|
6
|
+
output += `${tweet.text}\n`;
|
|
7
|
+
if (showMetrics) {
|
|
8
|
+
const metrics = [];
|
|
9
|
+
if (tweet.replyCount > 0)
|
|
10
|
+
metrics.push(`${tweet.replyCount} replies`);
|
|
11
|
+
if (tweet.retweetCount > 0)
|
|
12
|
+
metrics.push(`${tweet.retweetCount} retweets`);
|
|
13
|
+
if (tweet.likeCount > 0)
|
|
14
|
+
metrics.push(`${tweet.likeCount} likes`);
|
|
15
|
+
if (metrics.length > 0)
|
|
16
|
+
output += `\n${metrics.join(' · ')}`;
|
|
17
|
+
}
|
|
18
|
+
output += `\nhttps://x.com/${tweet.author.username}/status/${tweet.id}`;
|
|
19
|
+
return output;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Format a user for display
|
|
23
|
+
*/
|
|
24
|
+
export function formatUser(user) {
|
|
25
|
+
let output = `@${user.username} (${user.name})\n`;
|
|
26
|
+
if (user.description) {
|
|
27
|
+
output += `${user.description}\n`;
|
|
28
|
+
}
|
|
29
|
+
const metrics = [];
|
|
30
|
+
if (user.followersCount)
|
|
31
|
+
metrics.push(`${user.followersCount} followers`);
|
|
32
|
+
if (user.followingCount)
|
|
33
|
+
metrics.push(`${user.followingCount} following`);
|
|
34
|
+
if (metrics.length > 0)
|
|
35
|
+
output += `\n${metrics.join(' · ')}`;
|
|
36
|
+
if (user.isBlueVerified)
|
|
37
|
+
output += '\n✓ Blue Verified';
|
|
38
|
+
return output;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Format a news item for display
|
|
42
|
+
*/
|
|
43
|
+
export function formatNewsItem(item, index) {
|
|
44
|
+
let output = `${index + 1}. ${item.headline}`;
|
|
45
|
+
if (item.category) {
|
|
46
|
+
output += ` [${item.category}]`;
|
|
47
|
+
}
|
|
48
|
+
if (item.timeAgo) {
|
|
49
|
+
output += ` · ${item.timeAgo}`;
|
|
50
|
+
}
|
|
51
|
+
if (item.postCount) {
|
|
52
|
+
output += ` · ${item.postCount} posts`;
|
|
53
|
+
}
|
|
54
|
+
return output;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Format error message
|
|
58
|
+
*/
|
|
59
|
+
export function formatError(error, code) {
|
|
60
|
+
let output = `Error: ${error}`;
|
|
61
|
+
if (code) {
|
|
62
|
+
output += ` (${code})`;
|
|
63
|
+
}
|
|
64
|
+
return output;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Print a separator line
|
|
68
|
+
*/
|
|
69
|
+
export function separator(char = '─', length = 40) {
|
|
70
|
+
return char.repeat(length);
|
|
71
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { TwitterClient } from './index.js';
|
|
3
|
+
const AUTH_TOKEN = process.env.TWITTER_AUTH_TOKEN || '';
|
|
4
|
+
const CT0 = process.env.TWITTER_CT0 || '';
|
|
5
|
+
if (!AUTH_TOKEN || !CT0) {
|
|
6
|
+
console.error('Error: TWITTER_AUTH_TOKEN and TWITTER_CT0 environment variables must be set');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const client = new TwitterClient({
|
|
10
|
+
authToken: AUTH_TOKEN,
|
|
11
|
+
ct0: CT0,
|
|
12
|
+
});
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const command = args[0];
|
|
15
|
+
async function main() {
|
|
16
|
+
switch (command) {
|
|
17
|
+
case 'whoami': {
|
|
18
|
+
const result = await client.whoami();
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
console.error(`Error: ${result.error}`);
|
|
21
|
+
// Provide helpful hints
|
|
22
|
+
if (result.code === 'AUTH_FAILED') {
|
|
23
|
+
console.error('\nAuthentication failed. Please set these environment variables:');
|
|
24
|
+
console.error(' export TWITTER_AUTH_TOKEN="your_auth_token"');
|
|
25
|
+
console.error(' export TWITTER_CT0="your_ct0_token"');
|
|
26
|
+
console.error('\nGet credentials from browser DevTools → Application → Cookies → twitter.com');
|
|
27
|
+
}
|
|
28
|
+
else if (result.code === 'STALE_QUERY_ID') {
|
|
29
|
+
console.error('\nQuery ID may be stale. Try updating:');
|
|
30
|
+
console.error(' bird update-query-ids');
|
|
31
|
+
}
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
console.log(`Logged in as: @${result.user.username} (${result.user.name})`);
|
|
35
|
+
console.log(`User ID: ${result.user.id}`);
|
|
36
|
+
if (result.user.followersCount !== undefined) {
|
|
37
|
+
console.log(`Followers: ${result.user.followersCount.toLocaleString()}`);
|
|
38
|
+
}
|
|
39
|
+
if (result.user.followingCount !== undefined) {
|
|
40
|
+
console.log(`Following: ${result.user.followingCount.toLocaleString()}`);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case 'search': {
|
|
45
|
+
const latestFlag = args.includes('--latest');
|
|
46
|
+
const filteredArgs = args.filter(arg => arg !== '--latest');
|
|
47
|
+
const query = filteredArgs[1];
|
|
48
|
+
if (!query) {
|
|
49
|
+
console.error('Usage: bun run src/cli.ts search [--latest] <query>');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const result = await client.search(query, 20, latestFlag ? 'Latest' : 'Top');
|
|
53
|
+
if (result.success) {
|
|
54
|
+
result.tweets.forEach((tweet) => {
|
|
55
|
+
console.log(`@${tweet.author.username}: ${tweet.text}`);
|
|
56
|
+
console.log(`Likes: ${tweet.likeCount} Retweets: ${tweet.retweetCount}\n`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.error(`Error: ${result.error}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case 'tweet': {
|
|
66
|
+
const text = args.slice(1).join(' ');
|
|
67
|
+
if (!text) {
|
|
68
|
+
console.error('Usage: bun run src/cli.ts tweet <text>');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const result = await client.tweet(text);
|
|
72
|
+
if (result.success) {
|
|
73
|
+
console.log(`Tweet posted! ID: ${result.tweetId}`);
|
|
74
|
+
console.log(`https://x.com/user/status/${result.tweetId}`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Check for error 226 (automated detection)
|
|
78
|
+
if (result.error?.includes('226') || result.error?.includes('automated')) {
|
|
79
|
+
console.error('✗ Tweet rejected by Twitter: "Looks automated"');
|
|
80
|
+
console.error('');
|
|
81
|
+
console.error('This usually means:');
|
|
82
|
+
console.error(' • Account is new or has low activity');
|
|
83
|
+
console.error(' • Tweets need more variation/personal touch');
|
|
84
|
+
console.error(' • Try posting from the web interface first');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
console.error(`✗ Error: ${result.error}`);
|
|
88
|
+
}
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case 'get': {
|
|
94
|
+
const tweetId = args[1];
|
|
95
|
+
if (!tweetId) {
|
|
96
|
+
console.error('Usage: bun run src/cli.ts get <tweet_id_or_url>');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const result = await client.getTweet(tweetId);
|
|
100
|
+
if (result.success) {
|
|
101
|
+
const tweet = result.tweet;
|
|
102
|
+
console.log(`@${tweet.author.username} (@${tweet.author.name})`);
|
|
103
|
+
console.log(`${tweet.text}`);
|
|
104
|
+
console.log(`\nLikes: ${tweet.likeCount} Retweets: ${tweet.retweetCount} Replies: ${tweet.replyCount}`);
|
|
105
|
+
console.log(`https://x.com/${tweet.author.username}/status/${tweet.id}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.error(`Error: ${result.error}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case 'user': {
|
|
114
|
+
const username = args[1];
|
|
115
|
+
if (!username) {
|
|
116
|
+
console.error('Usage: bun run src/cli.ts user <username>');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const result = await client.getUserTweets(username);
|
|
120
|
+
if (result.success) {
|
|
121
|
+
result.tweets.forEach((tweet) => {
|
|
122
|
+
console.log(`@${tweet.author.username}: ${tweet.text}`);
|
|
123
|
+
console.log(`Likes: ${tweet.likeCount} Retweets: ${tweet.retweetCount}\n`);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.error(`Error: ${result.error}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'news': {
|
|
133
|
+
const result = await client.getNews();
|
|
134
|
+
if (result.success) {
|
|
135
|
+
result.items.forEach((item, i) => {
|
|
136
|
+
console.log(`${i + 1}. ${item.headline}`);
|
|
137
|
+
if (item.tweets && item.tweets[0]) {
|
|
138
|
+
console.log(` @${item.tweets[0].author.username}\n`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.error(`Error: ${result.error}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 'mentions': {
|
|
149
|
+
const result = await client.getMentions();
|
|
150
|
+
if (result.success) {
|
|
151
|
+
result.tweets.forEach((tweet) => {
|
|
152
|
+
console.log(`@${tweet.author.username}: ${tweet.text}`);
|
|
153
|
+
console.log(`Likes: ${tweet.likeCount} Retweets: ${tweet.retweetCount}\n`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.error(`Error: ${result.error}`);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'update-query-ids': {
|
|
163
|
+
const { QueryIdManager } = await import('./client/query-ids.js');
|
|
164
|
+
const manager = new QueryIdManager();
|
|
165
|
+
const result = await manager.updateQueryIds({ force: args.includes('--fresh') });
|
|
166
|
+
if (result.updated === 0) {
|
|
167
|
+
console.log('Query IDs are up to date (cache is fresh).');
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log(`✓ Updated ${result.updated} query ID(s):`);
|
|
171
|
+
result.changes.forEach(change => {
|
|
172
|
+
console.log(` ${change}`);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
console.log('Twitter Client CLI');
|
|
179
|
+
console.log('\nCommands:');
|
|
180
|
+
console.log(' whoami - Show current user info');
|
|
181
|
+
console.log(' search [--latest] <query> - Search for tweets (default: top, use --latest for recent)');
|
|
182
|
+
console.log(' tweet <text> - Post a tweet');
|
|
183
|
+
console.log(' get <tweet_id> - Get a tweet by ID or URL');
|
|
184
|
+
console.log(' user <username> - Get tweets from a user');
|
|
185
|
+
console.log(' news - Get news timeline');
|
|
186
|
+
console.log(' mentions - Get mentions timeline');
|
|
187
|
+
console.log(' update-query-ids [--fresh] - Update query IDs from Twitter');
|
|
188
|
+
console.log('\nFlags:');
|
|
189
|
+
console.log(' --fresh - Force refresh even if cache is fresh');
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
main().catch((err) => {
|
|
194
|
+
console.error(err);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { TweetResult, TweetsResult, PostResult, UserResult, SearchResult, NewsResult, ThreadResult } from './types.js';
|
|
2
|
+
export declare class TwitterClient {
|
|
3
|
+
private graphql;
|
|
4
|
+
private rest;
|
|
5
|
+
constructor(options: {
|
|
6
|
+
authToken: string;
|
|
7
|
+
ct0: string;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Verify authentication and get current user info
|
|
12
|
+
*/
|
|
13
|
+
whoami(): Promise<UserResult>;
|
|
14
|
+
/**
|
|
15
|
+
* Search for tweets
|
|
16
|
+
*/
|
|
17
|
+
search(query: string, count?: number, product?: 'Top' | 'Latest'): Promise<SearchResult>;
|
|
18
|
+
/**
|
|
19
|
+
* Get a single tweet by ID or URL
|
|
20
|
+
*/
|
|
21
|
+
getTweet(tweetIdOrUrl: string): Promise<TweetResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Get thread (conversation) for a tweet
|
|
24
|
+
*/
|
|
25
|
+
getThread(tweetIdOrUrl: string): Promise<ThreadResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Get tweets from a user's timeline
|
|
28
|
+
*/
|
|
29
|
+
getUserTweets(username: string, count?: number, cursor?: string): Promise<TweetsResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Post a new tweet
|
|
32
|
+
*/
|
|
33
|
+
tweet(text: string): Promise<PostResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Reply to a tweet
|
|
36
|
+
*/
|
|
37
|
+
reply(tweetId: string, text: string): Promise<PostResult>;
|
|
38
|
+
/**
|
|
39
|
+
* Get news/for you timeline
|
|
40
|
+
*/
|
|
41
|
+
getNews(count?: number): Promise<NewsResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Get mentions timeline
|
|
44
|
+
*/
|
|
45
|
+
getMentions(count?: number): Promise<TweetsResult>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=TwitterClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TwitterClient.d.ts","sourceRoot":"","sources":["../../src/client/TwitterClient.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,WAAW,EACX,YAAY,EACZ,UAAU,EACV,UAAU,EACV,YAAY,EACZ,UAAU,EACV,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,IAAI,CAAa;gBAEb,OAAO,EAAE;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB;IAeD;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC;IAyCnC;;OAEG;IACG,MAAM,CACV,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAW,EAClB,OAAO,GAAE,KAAK,GAAG,QAAgB,GAChC,OAAO,CAAC,YAAY,CAAC;IAqFxB;;OAEG;IACG,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAiD1D;;OAEG;IACG,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAe5D;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IA4HjG;;OAEG;IACG,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA2E9C;;OAEG;IACG,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA6D/D;;OAEG;IACG,OAAO,CAAC,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAmDtD;;OAEG;IACG,WAAW,CAAC,KAAK,GAAE,MAAW,GAAG,OAAO,CAAC,YAAY,CAAC;CA2C7D"}
|