rettiwt-api 1.1.5 → 1.1.8
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/dist/graphql/enums/Errors.d.ts +1 -0
- package/dist/graphql/enums/Errors.js +1 -0
- package/dist/graphql/enums/Errors.js.map +1 -1
- package/dist/graphql/resolvers/UserResolver.d.ts +9 -0
- package/dist/graphql/resolvers/UserResolver.js +61 -0
- package/dist/graphql/resolvers/UserResolver.js.map +1 -1
- package/dist/graphql/types/UserTypes.js +1 -1
- package/dist/graphql/types/UserTypes.js.map +1 -1
- package/dist/requests/payloads/Variables.d.ts +23 -0
- package/dist/requests/payloads/Variables.js +24 -0
- package/dist/requests/payloads/Variables.js.map +1 -0
- package/dist/services/auth/AccountService.d.ts +6 -11
- package/dist/services/auth/AccountService.js +91 -71
- package/dist/services/auth/AccountService.js.map +1 -1
- package/dist/services/data/TweetService.d.ts +1 -1
- package/dist/services/data/TweetService.js +1 -1
- package/dist/services/data/TweetService.js.map +1 -1
- package/dist/services/data/UserService.d.ts +15 -0
- package/dist/services/data/UserService.js +33 -0
- package/dist/services/data/UserService.js.map +1 -1
- package/dist/services/helper/extractors/Users.d.ts +6 -0
- package/dist/services/helper/extractors/Users.js +53 -1
- package/dist/services/helper/extractors/Users.js.map +1 -1
- package/dist/services/helper/urls/Users.d.ts +8 -1
- package/dist/services/helper/urls/Users.js +12 -2
- package/dist/services/helper/urls/Users.js.map +1 -1
- package/dist/types/Query.d.ts +80 -0
- package/dist/types/Query.js +3 -0
- package/dist/types/Query.js.map +1 -0
- package/dist/types/raw/data/tweet/Favouriters.d.ts +164 -0
- package/dist/types/raw/data/tweet/Favouriters.js +3 -0
- package/dist/types/raw/data/tweet/Favouriters.js.map +1 -0
- package/dist/types/raw/data/tweet/Retweeters.d.ts +171 -0
- package/dist/types/raw/data/tweet/Retweeters.js +3 -0
- package/dist/types/raw/data/tweet/Retweeters.js.map +1 -0
- package/dist/types/raw/data/tweet/Tweet.d.ts +746 -0
- package/dist/types/raw/data/tweet/Tweet.js +3 -0
- package/dist/types/raw/data/tweet/Tweet.js.map +1 -0
- package/dist/types/raw/data/tweet/Tweets.d.ts +386 -0
- package/dist/types/raw/data/tweet/Tweets.js +3 -0
- package/dist/types/raw/data/tweet/Tweets.js.map +1 -0
- package/dist/types/raw/data/user/Followers.d.ts +176 -0
- package/dist/types/raw/data/user/Followers.js +3 -0
- package/dist/types/raw/data/user/Followers.js.map +1 -0
- package/dist/types/raw/data/user/Following.d.ts +176 -0
- package/dist/types/raw/data/user/Following.js +3 -0
- package/dist/types/raw/data/user/Following.js.map +1 -0
- package/dist/types/raw/data/user/Likes.d.ts +1059 -0
- package/dist/types/raw/data/user/Likes.js +3 -0
- package/dist/types/raw/data/user/Likes.js.map +1 -0
- package/dist/types/raw/data/user/User.d.ts +117 -0
- package/dist/types/raw/data/user/User.js +3 -0
- package/dist/types/raw/data/user/User.js.map +1 -0
- package/dist/types/raw/query/tweet/Details.d.ts +80 -0
- package/dist/types/raw/query/tweet/Details.js +5 -0
- package/dist/types/raw/query/tweet/Details.js.map +1 -0
- package/dist/types/raw/query/tweet/Engagements.d.ts +29 -0
- package/dist/types/raw/query/tweet/Engagements.js +3 -0
- package/dist/types/raw/query/tweet/Engagements.js.map +1 -0
- package/dist/types/raw/query/tweet/Likes.d.ts +29 -0
- package/dist/types/raw/query/tweet/Likes.js +3 -0
- package/dist/types/raw/query/tweet/Likes.js.map +1 -0
- package/dist/types/raw/query/tweet/Retweets.d.ts +29 -0
- package/dist/types/raw/query/tweet/Retweets.js +3 -0
- package/dist/types/raw/query/tweet/Retweets.js.map +1 -0
- package/dist/types/raw/query/tweet/Search.d.ts +40 -0
- package/dist/types/raw/query/tweet/Search.js +3 -0
- package/dist/types/raw/query/tweet/Search.js.map +1 -0
- package/dist/types/raw/query/tweet/TweetLike.d.ts +29 -0
- package/dist/types/raw/query/tweet/TweetLike.js +3 -0
- package/dist/types/raw/query/tweet/TweetLike.js.map +1 -0
- package/dist/types/raw/query/tweet/TweetLikes.d.ts +29 -0
- package/dist/types/raw/query/tweet/TweetLikes.js +3 -0
- package/dist/types/raw/query/tweet/TweetLikes.js.map +1 -0
- package/dist/types/raw/query/tweet/TweetRetweets.d.ts +0 -0
- package/dist/types/raw/query/tweet/TweetRetweets.js +2 -0
- package/dist/types/raw/query/tweet/TweetRetweets.js.map +1 -0
- package/dist/types/raw/query/user/Details.d.ts +34 -0
- package/dist/types/raw/query/user/Details.js +3 -0
- package/dist/types/raw/query/user/Details.js.map +1 -0
- package/dist/types/raw/user/Tweets.d.ts +668 -1584
- package/dist/types/raw/user/User.js.map +1 -1
- package/package.json +2 -2
- package/src/graphql/enums/Errors.ts +1 -0
- package/src/graphql/resolvers/UserResolver.ts +58 -1
- package/src/graphql/types/UserTypes.ts +3 -3
- package/src/services/auth/AccountService.ts +79 -53
- package/src/services/data/TweetService.ts +5 -5
- package/src/services/data/UserService.ts +34 -0
- package/src/services/helper/extractors/Users.ts +55 -0
- package/src/services/helper/urls/Users.ts +39 -23
- package/src/types/raw/user/Tweets.ts +1747 -0
- package/docs/.nojekyll +0 -1
- package/docs/assets/highlight.css +0 -64
- package/docs/assets/main.js +0 -58
- package/docs/assets/search.js +0 -1
- package/docs/assets/style.css +0 -1280
- package/docs/classes/AccountService.html +0 -303
- package/docs/classes/AuthCookie.html +0 -146
- package/docs/classes/AuthService.html +0 -147
- package/docs/classes/CacheService.html +0 -157
- package/docs/classes/Cursor.html +0 -102
- package/docs/classes/CursoredData.html +0 -126
- package/docs/classes/DataValidationError.html +0 -119
- package/docs/classes/FetcherService.html +0 -225
- package/docs/classes/Tweet.html +0 -210
- package/docs/classes/TweetEntities.html +0 -128
- package/docs/classes/TweetFilter.html +0 -204
- package/docs/classes/TweetListArgs.html +0 -118
- package/docs/classes/TweetService.html +0 -313
- package/docs/classes/User.html +0 -230
- package/docs/classes/UserListArgs.html +0 -118
- package/docs/classes/UserService.html +0 -315
- package/docs/enums/HttpMethods.html +0 -74
- package/docs/functions/Rettiwt.html +0 -99
- package/docs/index.html +0 -161
- package/docs/interfaces/IAuthCookie.html +0 -104
- package/docs/interfaces/ICursor.html +0 -77
- package/docs/interfaces/ICursoredData.html +0 -93
- package/docs/interfaces/IDataContext.html +0 -91
- package/docs/interfaces/IListArgs.html +0 -87
- package/docs/interfaces/ITweet.html +0 -176
- package/docs/interfaces/ITweetEntities.html +0 -104
- package/docs/interfaces/ITweetFilter.html +0 -158
- package/docs/interfaces/IUser.html +0 -194
- package/docs/modules.html +0 -109
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"User.js","sourceRoot":"","sources":["../../../../src/types/raw/
|
|
1
|
+
{"version":3,"file":"User.js","sourceRoot":"","sources":["../../../../src/types/raw/user/User.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rettiwt-api",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"description": "An API for fetching data from TwitterAPI, without any rate limits!",
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"typedoc": "0.23.26",
|
|
44
44
|
"typescript": "4.6.4"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|
|
@@ -16,6 +16,7 @@ export enum DataErrors {
|
|
|
16
16
|
NoTweetsFound = "No tweets matching the given criteria found",
|
|
17
17
|
NoLikersFound = "No likers found for the tweet with the given id",
|
|
18
18
|
NoRetweetersFound = "No retweeters found for the tweet with the given id",
|
|
19
|
+
NoUserTweetsFound = "No tweets were found for the user with the given id",
|
|
19
20
|
NoFollowsFound = "No follow details were found for the user with the given id",
|
|
20
21
|
NoLikedTweetsFound = "No liked tweets were found for the user with the given id"
|
|
21
22
|
};
|
|
@@ -9,7 +9,7 @@ import { DataErrors } from '../enums/Errors';
|
|
|
9
9
|
export default class UserResolver extends ResolverBase {
|
|
10
10
|
// MEMBER DATA
|
|
11
11
|
private batchSize: number; // To store the batch size when fetching data
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
// MEMBER METHODS
|
|
14
14
|
constructor(context: IDataContext) {
|
|
15
15
|
super(context);
|
|
@@ -27,6 +27,63 @@ export default class UserResolver extends ResolverBase {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @returns The list of tweets made by the given user
|
|
32
|
+
* @param id The id of the user whose tweets are to be fetched
|
|
33
|
+
* @param count The number of tweets to fetch, must be >= 40
|
|
34
|
+
* @param all Whether to fetch list of all tweets made by user
|
|
35
|
+
* @param cursor The cursor to the batch of tweets to fetch
|
|
36
|
+
* @param statusesCount The total number of tweets made by target user
|
|
37
|
+
*/
|
|
38
|
+
async resolveUserTweets(id: string, count: number, all: boolean, cursor: string, statusesCount: number): Promise<any> {
|
|
39
|
+
let likes: any[] = []; // To store the list of tweets
|
|
40
|
+
let next: Cursor = new Cursor(cursor); // To store cursor to next batch
|
|
41
|
+
let total: number = 0; // To store the total number of tweets fetched
|
|
42
|
+
|
|
43
|
+
// If all tweets are to be fetched
|
|
44
|
+
count = all ? statusesCount : count;
|
|
45
|
+
|
|
46
|
+
// If required count less than batch size, setting batch size to required count
|
|
47
|
+
this.batchSize = (count < this.batchSize) ? count : this.batchSize;
|
|
48
|
+
|
|
49
|
+
// Repeatedly fetching data as long as total data fetched is less than requried
|
|
50
|
+
do {
|
|
51
|
+
// If this is the last batch, change batch size to number of remaining tweets
|
|
52
|
+
this.batchSize = ((count - total) < this.batchSize) ? (count - total) : this.batchSize;
|
|
53
|
+
|
|
54
|
+
// Getting the data
|
|
55
|
+
const res = await this.context.users.getUserTweets(id, this.batchSize, next.value).catch(error => {
|
|
56
|
+
throw this.getGraphQLError(error);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// If data is available
|
|
60
|
+
if (res.list?.length) {
|
|
61
|
+
// Adding fetched tweets to list of tweets
|
|
62
|
+
likes = likes.concat(res.list);
|
|
63
|
+
|
|
64
|
+
// Updating total tweets fetched
|
|
65
|
+
total = likes.length;
|
|
66
|
+
|
|
67
|
+
// Getting cursor to next batch
|
|
68
|
+
next = res.next as Cursor;
|
|
69
|
+
}
|
|
70
|
+
// If no more data is available
|
|
71
|
+
else {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
} while (total < count);
|
|
75
|
+
|
|
76
|
+
// If no likes found
|
|
77
|
+
if (!likes.length) {
|
|
78
|
+
return new Error(DataErrors.NoUserTweetsFound);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Adding the cursor to the end of list of data
|
|
82
|
+
likes.push(next);
|
|
83
|
+
|
|
84
|
+
return likes;
|
|
85
|
+
}
|
|
86
|
+
|
|
30
87
|
/**
|
|
31
88
|
* @returns The list of tweets liked by the given user
|
|
32
89
|
* @param id The id of the user whose likes are to be fetched
|
|
@@ -113,7 +113,7 @@ export const User: GraphQLObjectType = new GraphQLObjectType({
|
|
|
113
113
|
defaultValue: ''
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
|
-
resolve: (parent, args, context) => new
|
|
116
|
+
resolve: (parent, args, context) => new UserResolver(context).resolveUserTweets(parent.id, args.count, args.all, args.cursor, parent.statusesCount)
|
|
117
117
|
}
|
|
118
118
|
})
|
|
119
119
|
});
|
|
@@ -124,11 +124,11 @@ export const UserList: GraphQLList<GraphQLType> = new GraphQLList(new GraphQLUni
|
|
|
124
124
|
types: [User, Cursor],
|
|
125
125
|
resolveType: (data) => {
|
|
126
126
|
// If it has a userName field => this is a User object
|
|
127
|
-
if(data.userName) {
|
|
127
|
+
if (data.userName) {
|
|
128
128
|
return User;
|
|
129
129
|
}
|
|
130
130
|
// If it has a value field => this is a Cursor object
|
|
131
|
-
else if(data.value) {
|
|
131
|
+
else if (data.value) {
|
|
132
132
|
return Cursor;
|
|
133
133
|
}
|
|
134
134
|
}
|
|
@@ -22,23 +22,25 @@ import { Cookie, CookieJar } from 'cookiejar';
|
|
|
22
22
|
*/
|
|
23
23
|
export class AccountService {
|
|
24
24
|
/** The AuthService instance to use for authentication. */
|
|
25
|
-
private auth: AuthService;
|
|
26
|
-
|
|
25
|
+
private auth: AuthService = new AuthService();
|
|
26
|
+
|
|
27
27
|
/** The current guest credentials to use. */
|
|
28
|
-
private guestCreds: IGuestCredentials;
|
|
28
|
+
private guestCreds: IGuestCredentials = { authToken: '', guestToken: '' };
|
|
29
|
+
|
|
30
|
+
/** The email id of Twitter account to be logged into. */
|
|
31
|
+
private email: string = '';
|
|
32
|
+
|
|
33
|
+
/** The user name of the Twitter account ot be logged into */
|
|
34
|
+
private userName: string = '';
|
|
35
|
+
|
|
36
|
+
/** The password to the Twitter account to be logged into. */
|
|
37
|
+
private password: string = '';
|
|
29
38
|
|
|
30
39
|
/** The cookies received from Twitter after logging in. */
|
|
31
|
-
private cookies: Cookie[];
|
|
40
|
+
private cookies: Cookie[] = [];
|
|
32
41
|
|
|
33
42
|
/** The flow token received after execution of current flow. */
|
|
34
|
-
private flowToken: string;
|
|
35
|
-
|
|
36
|
-
constructor() {
|
|
37
|
-
this.auth = new AuthService();
|
|
38
|
-
this.guestCreds = { authToken: '', guestToken: '' };
|
|
39
|
-
this.cookies = [];
|
|
40
|
-
this.flowToken = '';
|
|
41
|
-
}
|
|
43
|
+
private flowToken: string = '';
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* @returns The current guest credentials to use. If if does not exists, then a new one is created
|
|
@@ -69,6 +71,9 @@ export class AccountService {
|
|
|
69
71
|
|
|
70
72
|
// Getting the flow token
|
|
71
73
|
this.flowToken = res.data['flow_token'];
|
|
74
|
+
|
|
75
|
+
// Executing next subtask
|
|
76
|
+
await this.jsInstrumentationSubtask();
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
/**
|
|
@@ -76,7 +81,7 @@ export class AccountService {
|
|
|
76
81
|
* @internal
|
|
77
82
|
*/
|
|
78
83
|
private async jsInstrumentationSubtask(): Promise<void> {
|
|
79
|
-
// Executing the
|
|
84
|
+
// Executing the subtask
|
|
80
85
|
const res: CurlyResult = await curly.post(LoginFlows.JsInstrumentationSubtask.url, {
|
|
81
86
|
httpHeader: loginHeader(await this.getGuestCredentials(), this.cookies.join(';').toString()),
|
|
82
87
|
sslVerifyPeer: false,
|
|
@@ -85,6 +90,9 @@ export class AccountService {
|
|
|
85
90
|
|
|
86
91
|
// Getting the flow token
|
|
87
92
|
this.flowToken = res.data['flow_token'];
|
|
93
|
+
|
|
94
|
+
// Executing next subtask
|
|
95
|
+
await this.enterUserIdentifier();
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
/**
|
|
@@ -93,12 +101,12 @@ export class AccountService {
|
|
|
93
101
|
*
|
|
94
102
|
* @throws {@link AuthenticationErrors.InvalidEmail}, if email does not exist.
|
|
95
103
|
*/
|
|
96
|
-
private async enterUserIdentifier(
|
|
97
|
-
// Executing the
|
|
104
|
+
private async enterUserIdentifier(): Promise<void> {
|
|
105
|
+
// Executing the subtask
|
|
98
106
|
const res: CurlyResult = await curly.post(LoginFlows.EnterUserIdentifier.url, {
|
|
99
107
|
httpHeader: loginHeader(await this.getGuestCredentials(), this.cookies.join(';').toString()),
|
|
100
108
|
sslVerifyPeer: false,
|
|
101
|
-
postFields: JSON.stringify(LoginFlows.EnterUserIdentifier.body(this.flowToken, email))
|
|
109
|
+
postFields: JSON.stringify(LoginFlows.EnterUserIdentifier.body(this.flowToken, this.email))
|
|
102
110
|
});
|
|
103
111
|
|
|
104
112
|
// If no account found with given email
|
|
@@ -108,6 +116,31 @@ export class AccountService {
|
|
|
108
116
|
|
|
109
117
|
// Getting the flow token
|
|
110
118
|
this.flowToken = res.data['flow_token'];
|
|
119
|
+
|
|
120
|
+
// Checking the next available subtasks
|
|
121
|
+
/**
|
|
122
|
+
* This subtask has two possible outcomes.
|
|
123
|
+
* 1. The server asks for a username next.
|
|
124
|
+
* 2. The server directly asks for password, skipping username check.
|
|
125
|
+
*
|
|
126
|
+
* So, checking which is the subtask required by server, and executing that particular subtask.
|
|
127
|
+
*/
|
|
128
|
+
for (let task of res.data.subtasks) {
|
|
129
|
+
// If next subtask is to enter username
|
|
130
|
+
if (task['subtask_id'] == 'LoginEnterAlternateIdentifierSubtask') {
|
|
131
|
+
// Executing next subtask
|
|
132
|
+
await this.enterAlternateUserIdentifier();
|
|
133
|
+
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
// If next subtask is to enter password
|
|
137
|
+
else if (task['subtask_id'] == 'LoginEnterPassword') {
|
|
138
|
+
// Executing next subtask
|
|
139
|
+
await this.enterPassword();
|
|
140
|
+
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
111
144
|
}
|
|
112
145
|
|
|
113
146
|
/**
|
|
@@ -116,12 +149,12 @@ export class AccountService {
|
|
|
116
149
|
*
|
|
117
150
|
* @throws {@link AuthenticationErrors.InvalidUsername}, if wrong username entered.
|
|
118
151
|
*/
|
|
119
|
-
private async enterAlternateUserIdentifier(
|
|
120
|
-
// Executing the
|
|
152
|
+
private async enterAlternateUserIdentifier(): Promise<void> {
|
|
153
|
+
// Executing the subtask
|
|
121
154
|
const res: CurlyResult = await curly.post(LoginFlows.EnterAlternateUserIdentifier.url, {
|
|
122
155
|
httpHeader: loginHeader(await this.getGuestCredentials(), this.cookies.join(';').toString()),
|
|
123
156
|
sslVerifyPeer: false,
|
|
124
|
-
postFields: JSON.stringify(LoginFlows.EnterAlternateUserIdentifier.body(this.flowToken, userName))
|
|
157
|
+
postFields: JSON.stringify(LoginFlows.EnterAlternateUserIdentifier.body(this.flowToken, this.userName))
|
|
125
158
|
});
|
|
126
159
|
|
|
127
160
|
// If invalid username for the given account
|
|
@@ -131,6 +164,9 @@ export class AccountService {
|
|
|
131
164
|
|
|
132
165
|
// Getting the flow token
|
|
133
166
|
this.flowToken = res.data['flow_token'];
|
|
167
|
+
|
|
168
|
+
// Executing next subtask
|
|
169
|
+
await this.enterPassword();
|
|
134
170
|
}
|
|
135
171
|
|
|
136
172
|
/**
|
|
@@ -139,12 +175,12 @@ export class AccountService {
|
|
|
139
175
|
*
|
|
140
176
|
* @throws {@link AuthenticationErrors.InvalidPassword}, incorrect password entered.
|
|
141
177
|
*/
|
|
142
|
-
private async enterPassword(
|
|
143
|
-
// Executing the
|
|
178
|
+
private async enterPassword(): Promise<void> {
|
|
179
|
+
// Executing the subtask
|
|
144
180
|
const res: CurlyResult = await curly.post(LoginFlows.EnterPassword.url, {
|
|
145
181
|
httpHeader: loginHeader(await this.getGuestCredentials(), this.cookies.join(';').toString()),
|
|
146
182
|
sslVerifyPeer: false,
|
|
147
|
-
postFields: JSON.stringify(LoginFlows.EnterPassword.body(this.flowToken, password))
|
|
183
|
+
postFields: JSON.stringify(LoginFlows.EnterPassword.body(this.flowToken, this.password))
|
|
148
184
|
});
|
|
149
185
|
|
|
150
186
|
// If invalid password for the given account
|
|
@@ -154,6 +190,9 @@ export class AccountService {
|
|
|
154
190
|
|
|
155
191
|
// Getting the flow token
|
|
156
192
|
this.flowToken = res.data['flow_token'];
|
|
193
|
+
|
|
194
|
+
// Executing next subtask
|
|
195
|
+
await this.accountDuplicationCheck();
|
|
157
196
|
}
|
|
158
197
|
|
|
159
198
|
/**
|
|
@@ -161,7 +200,7 @@ export class AccountService {
|
|
|
161
200
|
* @internal
|
|
162
201
|
*/
|
|
163
202
|
private async accountDuplicationCheck(): Promise<void> {
|
|
164
|
-
// Executing the
|
|
203
|
+
// Executing the subtask
|
|
165
204
|
const res: CurlyResult = await curly.post(LoginFlows.AccountDuplicationCheck.url, {
|
|
166
205
|
httpHeader: loginHeader(await this.getGuestCredentials(), this.cookies.join(';').toString()),
|
|
167
206
|
sslVerifyPeer: false,
|
|
@@ -175,31 +214,6 @@ export class AccountService {
|
|
|
175
214
|
this.flowToken = res.data['flow_token'];
|
|
176
215
|
}
|
|
177
216
|
|
|
178
|
-
/**
|
|
179
|
-
* Execute all the flows required to login to Twitter, using the given credentials, then set the response cookies.
|
|
180
|
-
*
|
|
181
|
-
* @internal
|
|
182
|
-
*
|
|
183
|
-
* @param email The email of the account to be logged into.
|
|
184
|
-
* @param userName The username associated with the given account.
|
|
185
|
-
* @param password The password to the account.
|
|
186
|
-
*/
|
|
187
|
-
private async executeLoginFlows(email: string, userName: string, password: string): Promise<void> {
|
|
188
|
-
/**
|
|
189
|
-
* This works by sending a chain of request that are required for login to twitter.
|
|
190
|
-
* Each method in the chain returns a flow token that must be provied as payload in the next method in the chain.
|
|
191
|
-
* Each such method is called a subtask.
|
|
192
|
-
* Each subtask sets the {@link flowToken} property of the class which is then given in the payload of the next subtask.
|
|
193
|
-
* The final subtask returns the headers which actually contains the cookie in the 'set-cookie' field.
|
|
194
|
-
*/
|
|
195
|
-
await this.initiateLogin();
|
|
196
|
-
await this.jsInstrumentationSubtask();
|
|
197
|
-
await this.enterUserIdentifier(email);
|
|
198
|
-
await this.enterAlternateUserIdentifier(userName);
|
|
199
|
-
await this.enterPassword(password);
|
|
200
|
-
await this.accountDuplicationCheck();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
217
|
/**
|
|
204
218
|
* Parse the authentication cookies recieved from Twitter into known format.
|
|
205
219
|
*
|
|
@@ -212,7 +226,7 @@ export class AccountService {
|
|
|
212
226
|
private parseCookies(cookies: Cookie[]): IAuthCookie {
|
|
213
227
|
/** The tempoorary parsed cookies. */
|
|
214
228
|
let tempCookies: any = {};
|
|
215
|
-
|
|
229
|
+
|
|
216
230
|
/**
|
|
217
231
|
* Parsing the cookies into a standard JSON format.
|
|
218
232
|
* The format is 'cookie_name': 'cookie_value'.
|
|
@@ -244,13 +258,25 @@ export class AccountService {
|
|
|
244
258
|
public async login(email: string, userName: string, password: string): Promise<IAuthCookie> {
|
|
245
259
|
/** The parsed cookies that will be returned. */
|
|
246
260
|
let parsedCookies: IAuthCookie;
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
|
|
261
|
+
|
|
262
|
+
// Setting user credentials
|
|
263
|
+
this.email = email;
|
|
264
|
+
this.userName = userName;
|
|
265
|
+
this.password = password;
|
|
266
|
+
|
|
267
|
+
// Initiating login
|
|
268
|
+
/**
|
|
269
|
+
* This works by sending a chain of request that are required for login to twitter.
|
|
270
|
+
* Each method in the chain returns a flow token that must be provied as payload in the next method in the chain.
|
|
271
|
+
* Each such method is called a subtask.
|
|
272
|
+
* Each subtask sets the {@link flowToken} property of the class which is used in the payload of the next subtask.
|
|
273
|
+
* The final subtask returns the headers which actually contains the cookie in the 'set-cookie' field.
|
|
274
|
+
*/
|
|
275
|
+
await this.initiateLogin();
|
|
250
276
|
|
|
251
277
|
// Parsing the cookies
|
|
252
278
|
parsedCookies = this.parseCookies(this.cookies);
|
|
253
|
-
|
|
279
|
+
|
|
254
280
|
// Returning the final parsed cookies
|
|
255
281
|
return parsedCookies;
|
|
256
282
|
}
|
|
@@ -51,7 +51,7 @@ export class TweetService extends FetcherService {
|
|
|
51
51
|
*
|
|
52
52
|
* @remarks
|
|
53
53
|
*
|
|
54
|
-
*
|
|
54
|
+
* Cookies are required to use this method!
|
|
55
55
|
*/
|
|
56
56
|
async getTweets(query: TweetFilter, count?: number, cursor?: string): Promise<CursoredData<Tweet>> {
|
|
57
57
|
// Objectifying parameters
|
|
@@ -95,7 +95,7 @@ export class TweetService extends FetcherService {
|
|
|
95
95
|
if (cachedData) {
|
|
96
96
|
return cachedData;
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
// Fetching the raw data
|
|
100
100
|
let res = await this.request<RawTweet>(TweetUrls.tweetDetailsUrl(id), false).then(res => res.data);
|
|
101
101
|
|
|
@@ -128,13 +128,13 @@ export class TweetService extends FetcherService {
|
|
|
128
128
|
*/
|
|
129
129
|
async getTweetLikers(tweetId: string, count?: number, cursor?: string): Promise<CursoredData<User>> {
|
|
130
130
|
// If user is not authenticated, abort
|
|
131
|
-
if(!this.isAuthenticated) {
|
|
131
|
+
if (!this.isAuthenticated) {
|
|
132
132
|
throw new Error(AuthenticationErrors.NotAuthenticated);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// Objectifying parameters
|
|
136
136
|
let args: TweetListArgs = new TweetListArgs(count, cursor);
|
|
137
|
-
|
|
137
|
+
|
|
138
138
|
// Fetching the raw data
|
|
139
139
|
let res = await this.request<RawLikers>(TweetUrls.tweetLikesUrl(tweetId, args.count, args.cursor)).then(res => res.data);
|
|
140
140
|
|
|
@@ -167,7 +167,7 @@ export class TweetService extends FetcherService {
|
|
|
167
167
|
*/
|
|
168
168
|
async getTweetRetweeters(tweetId: string, count?: number, cursor?: string): Promise<CursoredData<User>> {
|
|
169
169
|
// If user is not authenticated, abort
|
|
170
|
-
if(!this.isAuthenticated) {
|
|
170
|
+
if (!this.isAuthenticated) {
|
|
171
171
|
throw new Error(AuthenticationErrors.NotAuthenticated);
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -11,6 +11,7 @@ import { Tweet } from '../../models/data/Tweet';
|
|
|
11
11
|
import { CursoredData } from '../../models/data/CursoredData';
|
|
12
12
|
import { Result as TweetData } from '../../types/raw/tweet/Tweet';
|
|
13
13
|
import RawUser, { Result as UserData } from '../../types/raw/user/User';
|
|
14
|
+
import RawUserTweets from '../../types/raw/user/Tweets';
|
|
14
15
|
import RawUserFollowers from '../../types/raw/user/Followers';
|
|
15
16
|
import RawUserFollowing from '../../types/raw/user/Following';
|
|
16
17
|
import RawUserLikes from '../../types/raw/user/Likes';
|
|
@@ -80,6 +81,39 @@ export class UserService extends FetcherService {
|
|
|
80
81
|
return user;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @param userId The rest id of the target user.
|
|
86
|
+
* @param count The number of tweets to fetch, must be >= 40 (when no cursor is provided) and <=100.
|
|
87
|
+
* @param cursor The cursor to next batch. If blank, first batch is fetched.
|
|
88
|
+
*
|
|
89
|
+
* @returns The list of tweets nade by the target user.
|
|
90
|
+
*
|
|
91
|
+
* @throws {@link Errors.ValidationErrors.InvalidCount} error, if invalid count has been provided.
|
|
92
|
+
* @throws {@link Errors.DataErrors.UserNotFound} error, if invalid count has been provided.
|
|
93
|
+
*
|
|
94
|
+
* @remarks
|
|
95
|
+
*
|
|
96
|
+
* No cookies are required to use this method.
|
|
97
|
+
*/
|
|
98
|
+
async getUserTweets(userId: string, count?: number, cursor?: string): Promise<CursoredData<Tweet>> {
|
|
99
|
+
// Objectifying parameters
|
|
100
|
+
let args: UserListArgs = new UserListArgs(count, cursor);
|
|
101
|
+
|
|
102
|
+
// Fetching the raw data
|
|
103
|
+
let res = await this.request<RawUserTweets>(UserUrls.userTweetsUrl(userId, args.count, args.cursor), false).then(res => res.data);
|
|
104
|
+
|
|
105
|
+
// Extracting data
|
|
106
|
+
let data = UserExtractors.extractUserTweets(res);
|
|
107
|
+
|
|
108
|
+
// Caching data
|
|
109
|
+
this.cacheData(data);
|
|
110
|
+
|
|
111
|
+
// Parsing data
|
|
112
|
+
let tweets = data.required.map((item: TweetData) => new Tweet(item));
|
|
113
|
+
|
|
114
|
+
return new CursoredData<Tweet>(tweets, data.cursor);
|
|
115
|
+
}
|
|
116
|
+
|
|
83
117
|
/**
|
|
84
118
|
* @param userId The rest id of the target user.
|
|
85
119
|
* @param count The number of following to fetch, must be >= 40 (when no cursor is provided) and <=100.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { IDataExtract } from '../../../types/Resolvers'
|
|
3
3
|
import { DataErrors } from '../../../enums/Errors';
|
|
4
4
|
import RawUser from '../../../types/raw/user/User';
|
|
5
|
+
import RawUserTweets from '../../../types/raw/user/Tweets';
|
|
5
6
|
import RawUserFollowers from '../../../types/raw/user/Followers';
|
|
6
7
|
import RawUserFollowing from '../../../types/raw/user/Following';
|
|
7
8
|
import RawUserLikes from '../../../types/raw/user/Likes';
|
|
@@ -37,6 +38,60 @@ export function extractUserDetails(res: RawUser): IDataExtract {
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @returns The raw user tweet data formatted and sorted into required and additional data
|
|
43
|
+
* @param res The raw response received from Twitter
|
|
44
|
+
*/
|
|
45
|
+
export function extractUserTweets(res: RawUserTweets): IDataExtract {
|
|
46
|
+
let required: any[] = []; // To store the reqruied raw data
|
|
47
|
+
let cursor: string = ''; // To store the cursor to next batch
|
|
48
|
+
let users: any[] = []; // To store additional user data
|
|
49
|
+
let tweets: any[] = []; // To store additional tweet data
|
|
50
|
+
|
|
51
|
+
// If user does not exist
|
|
52
|
+
if (Parsers.isJSONEmpty(res.data.user)) {
|
|
53
|
+
throw new Error(DataErrors.UserNotFound);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extracting the raw list
|
|
57
|
+
res.data.user.result.timeline_v2.timeline.instructions.forEach(item => {
|
|
58
|
+
if (item.type === 'TimelineAddEntries') {
|
|
59
|
+
// If no tweets found
|
|
60
|
+
if (item.entries?.length == 2) {
|
|
61
|
+
// Returning the data
|
|
62
|
+
return {
|
|
63
|
+
required: required,
|
|
64
|
+
cursor: cursor,
|
|
65
|
+
users: users,
|
|
66
|
+
tweets: tweets
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Destructuring data
|
|
71
|
+
item.entries.forEach(entry => {
|
|
72
|
+
// If entry is of type tweet and tweet exists
|
|
73
|
+
if (entry.entryId.indexOf('tweet') != -1 && entry.content.itemContent?.tweet_results.result.__typename === 'Tweet') {
|
|
74
|
+
required.push(entry.content.itemContent.tweet_results.result);
|
|
75
|
+
users.push(entry.content.itemContent.tweet_results.result.core.user_results.result);
|
|
76
|
+
tweets.push(entry.content.itemContent.tweet_results.result);
|
|
77
|
+
}
|
|
78
|
+
// If entry is of type cursor
|
|
79
|
+
else if (entry.entryId.indexOf('cursor-bottom') != -1) {
|
|
80
|
+
cursor = entry.content.value ?? '';
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Returning the data
|
|
87
|
+
return {
|
|
88
|
+
required: required,
|
|
89
|
+
cursor: cursor,
|
|
90
|
+
users: users,
|
|
91
|
+
tweets: tweets
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
40
95
|
/**
|
|
41
96
|
* @returns The raw user following/followers data formatted and sorted into required and additional data
|
|
42
97
|
* @param res The raw response received from TwitterAPI
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @param screenName The screen name of the target user
|
|
4
4
|
*/
|
|
5
5
|
export function userDetailsUrl(screenName: string): string {
|
|
6
|
-
|
|
6
|
+
return `https://api.twitter.com/graphql/hVhfo_TquFTmgL7gYwf91Q/UserByScreenName?variables=%7B%22screen_name%22%3A%22${screenName}%22%2C%22withSafetyModeUserFields%22%3Atrue%2C%22withSuperFollowsUserFields%22%3Atrue%7D&features=%7B%22responsive_web_twitter_blue_verified_badge_is_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%7D`;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -11,23 +11,35 @@ export function userDetailsUrl(screenName: string): string {
|
|
|
11
11
|
* @param restid The restId of the target user
|
|
12
12
|
*/
|
|
13
13
|
export function userDetailsByIdUrl(restId: string): string {
|
|
14
|
-
|
|
14
|
+
return `https://api.twitter.com/graphql/mi_IjXgFyr41N9zkszPz9w/UserByRestId?variables=%7B%22userId%22%3A%22${restId}%22%2C%22withSafetyModeUserFields%22%3Atrue%2C%22withSuperFollowsUserFields%22%3Atrue%7D&features=%7B%22responsive_web_twitter_blue_verified_badge_is_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%7D`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* @returns The url for fetching the list of
|
|
18
|
+
* @returns The url for fetching the list of tweet made by target user.
|
|
19
|
+
* @param userId The rest id of the target user
|
|
20
|
+
* @param count The batch size of the list of tweets, should be >= 40 and <=100
|
|
21
|
+
* @param cursor The cursor to next batch
|
|
22
|
+
*/
|
|
23
|
+
export function userTweetsUrl(userId: string, count: number, cursor: string): string {
|
|
24
|
+
return `https://api.twitter.com/graphql/xxLjoOBBPpYBHbBTI-hevQ/UserTweetsAndReplies?variables=%7B%22userId%22%3A%22${userId}%22%2C%22count%22%3A${count}%2C%22cursor%22%3A%22${cursor}%22%2C%22includePromotedContent%22%3Atrue%2C%22withCommunity%22%3Atrue%2C%22withSuperFollowsUserFields%22%3Atrue%2C%22withDownvotePerspective%22%3Afalse%2C%22withReactionsMetadata%22%3Afalse%2C%22withReactionsPerspective%22%3Afalse%2C%22withSuperFollowsTweetFields%22%3Atrue%2C%22withVoice%22%3Atrue%2C%22withV2Timeline%22%3Atrue%7D&features=%7B%22responsive_web_twitter_blue_verified_badge_is_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22view_counts_public_visibility_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Afalse%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22vibe_api_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Afalse%2C%22interactive_text_enabled%22%3Atrue%2C%22responsive_web_text_conversations_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @returns The url for fetching the list of users followed by target user.
|
|
19
29
|
* @param userId The rest id of the target user
|
|
20
30
|
* @param count The batch size of the list of following, should be >= 40 and <=100
|
|
21
31
|
* @param cursor The cursor to next batch
|
|
22
32
|
*/
|
|
23
33
|
export function userFollowingUrl(userId: string, count: number, cursor: string): string {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Twitter has a ver odd behaviour here.
|
|
36
|
+
* If no cursor is provided, the number of followings fetched is slightly more the given count.
|
|
37
|
+
* If a cursor if provided, the number of followings is sometimes less than the provided count.
|
|
38
|
+
* NO SOLUTION EXISTS AS OF NOW!
|
|
39
|
+
*/
|
|
40
|
+
return `https://api.twitter.com/graphql/mSnjZc5CTm2Z5Lu_i4XsPQ/Following?variables=%7B%22userId%22%3A%22${userId}%22%2C%22count%22%3A${count}%2C%22cursor%22%3A%22${encodeURIComponent(
|
|
41
|
+
cursor,
|
|
42
|
+
)}%22%2C%22includePromotedContent%22%3Afalse%2C%22withSuperFollowsUserFields%22%3Atrue%2C%22withDownvotePerspective%22%3Afalse%2C%22withReactionsMetadata%22%3Afalse%2C%22withReactionsPerspective%22%3Afalse%2C%22withSuperFollowsTweetFields%22%3Atrue%7D&features=%7B%22responsive_web_twitter_blue_verified_badge_is_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22view_counts_public_visibility_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Afalse%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_uc_gql_enabled%22%3Atrue%2C%22vibe_api_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Afalse%2C%22interactive_text_enabled%22%3Atrue%2C%22responsive_web_text_conversations_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D`;
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
/**
|
|
@@ -37,18 +49,20 @@ export function userFollowingUrl(userId: string, count: number, cursor: string):
|
|
|
37
49
|
* @param cursor The cusor to next batch
|
|
38
50
|
*/
|
|
39
51
|
export function userFollowersUrl(userId: string, count: number, cursor: string): string {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Twitter has a very odd behaviour here.
|
|
54
|
+
* If no cursor is provided, the number of followers fetched is equal to count + 20.
|
|
55
|
+
* If a cursor is provided, the number of followers fetched is equal to count.
|
|
56
|
+
* The solution is to check accordingly, if a cursor if provided or not and manipulate the count
|
|
57
|
+
*/
|
|
58
|
+
// If no cursor if provided
|
|
59
|
+
if (!cursor) {
|
|
60
|
+
count = count - 20;
|
|
61
|
+
}
|
|
50
62
|
|
|
51
|
-
|
|
63
|
+
return `https://api.twitter.com/graphql/nwlAnaw7oKXcVLi91ehy7Q/Followers?variables=%7B%22userId%22%3A%22${userId}%22%2C%22count%22%3A${count}%2C%22cursor%22%3A%22${encodeURIComponent(
|
|
64
|
+
cursor,
|
|
65
|
+
)}%22%2C%22includePromotedContent%22%3Afalse%2C%22withSuperFollowsUserFields%22%3Atrue%2C%22withDownvotePerspective%22%3Afalse%2C%22withReactionsMetadata%22%3Afalse%2C%22withReactionsPerspective%22%3Afalse%2C%22withSuperFollowsTweetFields%22%3Atrue%7D&features=%7B%22responsive_web_twitter_blue_verified_badge_is_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22view_counts_public_visibility_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Afalse%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_uc_gql_enabled%22%3Atrue%2C%22vibe_api_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Afalse%2C%22interactive_text_enabled%22%3Atrue%2C%22responsive_web_text_conversations_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D`;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
/**
|
|
@@ -58,5 +72,7 @@ export function userFollowersUrl(userId: string, count: number, cursor: string):
|
|
|
58
72
|
* @param cursor The cusor to next batch
|
|
59
73
|
*/
|
|
60
74
|
export function userLikesUrl(userId: string, count: number, cursor: string): string {
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
return `https://api.twitter.com/graphql/gP4ZKghLd4tpILgS6VudAQ/Likes?variables=%7B%22userId%22%3A%22${userId}%22%2C%22count%22%3A${count}%2C%22cursor%22%3A%22${encodeURIComponent(
|
|
76
|
+
cursor,
|
|
77
|
+
)}%22%2C%22includePromotedContent%22%3Afalse%2C%22withSuperFollowsUserFields%22%3Atrue%2C%22withDownvotePerspective%22%3Afalse%2C%22withReactionsMetadata%22%3Afalse%2C%22withReactionsPerspective%22%3Afalse%2C%22withSuperFollowsTweetFields%22%3Atrue%2C%22withClientEventToken%22%3Afalse%2C%22withBirdwatchNotes%22%3Afalse%2C%22withVoice%22%3Atrue%2C%22withV2Timeline%22%3Atrue%7D&features=%7B%22responsive_web_twitter_blue_verified_badge_is_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22view_counts_public_visibility_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Afalse%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_uc_gql_enabled%22%3Atrue%2C%22vibe_api_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Afalse%2C%22interactive_text_enabled%22%3Atrue%2C%22responsive_web_text_conversations_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D`;
|
|
78
|
+
}
|