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.
Files changed (126) hide show
  1. package/dist/graphql/enums/Errors.d.ts +1 -0
  2. package/dist/graphql/enums/Errors.js +1 -0
  3. package/dist/graphql/enums/Errors.js.map +1 -1
  4. package/dist/graphql/resolvers/UserResolver.d.ts +9 -0
  5. package/dist/graphql/resolvers/UserResolver.js +61 -0
  6. package/dist/graphql/resolvers/UserResolver.js.map +1 -1
  7. package/dist/graphql/types/UserTypes.js +1 -1
  8. package/dist/graphql/types/UserTypes.js.map +1 -1
  9. package/dist/requests/payloads/Variables.d.ts +23 -0
  10. package/dist/requests/payloads/Variables.js +24 -0
  11. package/dist/requests/payloads/Variables.js.map +1 -0
  12. package/dist/services/auth/AccountService.d.ts +6 -11
  13. package/dist/services/auth/AccountService.js +91 -71
  14. package/dist/services/auth/AccountService.js.map +1 -1
  15. package/dist/services/data/TweetService.d.ts +1 -1
  16. package/dist/services/data/TweetService.js +1 -1
  17. package/dist/services/data/TweetService.js.map +1 -1
  18. package/dist/services/data/UserService.d.ts +15 -0
  19. package/dist/services/data/UserService.js +33 -0
  20. package/dist/services/data/UserService.js.map +1 -1
  21. package/dist/services/helper/extractors/Users.d.ts +6 -0
  22. package/dist/services/helper/extractors/Users.js +53 -1
  23. package/dist/services/helper/extractors/Users.js.map +1 -1
  24. package/dist/services/helper/urls/Users.d.ts +8 -1
  25. package/dist/services/helper/urls/Users.js +12 -2
  26. package/dist/services/helper/urls/Users.js.map +1 -1
  27. package/dist/types/Query.d.ts +80 -0
  28. package/dist/types/Query.js +3 -0
  29. package/dist/types/Query.js.map +1 -0
  30. package/dist/types/raw/data/tweet/Favouriters.d.ts +164 -0
  31. package/dist/types/raw/data/tweet/Favouriters.js +3 -0
  32. package/dist/types/raw/data/tweet/Favouriters.js.map +1 -0
  33. package/dist/types/raw/data/tweet/Retweeters.d.ts +171 -0
  34. package/dist/types/raw/data/tweet/Retweeters.js +3 -0
  35. package/dist/types/raw/data/tweet/Retweeters.js.map +1 -0
  36. package/dist/types/raw/data/tweet/Tweet.d.ts +746 -0
  37. package/dist/types/raw/data/tweet/Tweet.js +3 -0
  38. package/dist/types/raw/data/tweet/Tweet.js.map +1 -0
  39. package/dist/types/raw/data/tweet/Tweets.d.ts +386 -0
  40. package/dist/types/raw/data/tweet/Tweets.js +3 -0
  41. package/dist/types/raw/data/tweet/Tweets.js.map +1 -0
  42. package/dist/types/raw/data/user/Followers.d.ts +176 -0
  43. package/dist/types/raw/data/user/Followers.js +3 -0
  44. package/dist/types/raw/data/user/Followers.js.map +1 -0
  45. package/dist/types/raw/data/user/Following.d.ts +176 -0
  46. package/dist/types/raw/data/user/Following.js +3 -0
  47. package/dist/types/raw/data/user/Following.js.map +1 -0
  48. package/dist/types/raw/data/user/Likes.d.ts +1059 -0
  49. package/dist/types/raw/data/user/Likes.js +3 -0
  50. package/dist/types/raw/data/user/Likes.js.map +1 -0
  51. package/dist/types/raw/data/user/User.d.ts +117 -0
  52. package/dist/types/raw/data/user/User.js +3 -0
  53. package/dist/types/raw/data/user/User.js.map +1 -0
  54. package/dist/types/raw/query/tweet/Details.d.ts +80 -0
  55. package/dist/types/raw/query/tweet/Details.js +5 -0
  56. package/dist/types/raw/query/tweet/Details.js.map +1 -0
  57. package/dist/types/raw/query/tweet/Engagements.d.ts +29 -0
  58. package/dist/types/raw/query/tweet/Engagements.js +3 -0
  59. package/dist/types/raw/query/tweet/Engagements.js.map +1 -0
  60. package/dist/types/raw/query/tweet/Likes.d.ts +29 -0
  61. package/dist/types/raw/query/tweet/Likes.js +3 -0
  62. package/dist/types/raw/query/tweet/Likes.js.map +1 -0
  63. package/dist/types/raw/query/tweet/Retweets.d.ts +29 -0
  64. package/dist/types/raw/query/tweet/Retweets.js +3 -0
  65. package/dist/types/raw/query/tweet/Retweets.js.map +1 -0
  66. package/dist/types/raw/query/tweet/Search.d.ts +40 -0
  67. package/dist/types/raw/query/tweet/Search.js +3 -0
  68. package/dist/types/raw/query/tweet/Search.js.map +1 -0
  69. package/dist/types/raw/query/tweet/TweetLike.d.ts +29 -0
  70. package/dist/types/raw/query/tweet/TweetLike.js +3 -0
  71. package/dist/types/raw/query/tweet/TweetLike.js.map +1 -0
  72. package/dist/types/raw/query/tweet/TweetLikes.d.ts +29 -0
  73. package/dist/types/raw/query/tweet/TweetLikes.js +3 -0
  74. package/dist/types/raw/query/tweet/TweetLikes.js.map +1 -0
  75. package/dist/types/raw/query/tweet/TweetRetweets.d.ts +0 -0
  76. package/dist/types/raw/query/tweet/TweetRetweets.js +2 -0
  77. package/dist/types/raw/query/tweet/TweetRetweets.js.map +1 -0
  78. package/dist/types/raw/query/user/Details.d.ts +34 -0
  79. package/dist/types/raw/query/user/Details.js +3 -0
  80. package/dist/types/raw/query/user/Details.js.map +1 -0
  81. package/dist/types/raw/user/Tweets.d.ts +668 -1584
  82. package/dist/types/raw/user/User.js.map +1 -1
  83. package/package.json +2 -2
  84. package/src/graphql/enums/Errors.ts +1 -0
  85. package/src/graphql/resolvers/UserResolver.ts +58 -1
  86. package/src/graphql/types/UserTypes.ts +3 -3
  87. package/src/services/auth/AccountService.ts +79 -53
  88. package/src/services/data/TweetService.ts +5 -5
  89. package/src/services/data/UserService.ts +34 -0
  90. package/src/services/helper/extractors/Users.ts +55 -0
  91. package/src/services/helper/urls/Users.ts +39 -23
  92. package/src/types/raw/user/Tweets.ts +1747 -0
  93. package/docs/.nojekyll +0 -1
  94. package/docs/assets/highlight.css +0 -64
  95. package/docs/assets/main.js +0 -58
  96. package/docs/assets/search.js +0 -1
  97. package/docs/assets/style.css +0 -1280
  98. package/docs/classes/AccountService.html +0 -303
  99. package/docs/classes/AuthCookie.html +0 -146
  100. package/docs/classes/AuthService.html +0 -147
  101. package/docs/classes/CacheService.html +0 -157
  102. package/docs/classes/Cursor.html +0 -102
  103. package/docs/classes/CursoredData.html +0 -126
  104. package/docs/classes/DataValidationError.html +0 -119
  105. package/docs/classes/FetcherService.html +0 -225
  106. package/docs/classes/Tweet.html +0 -210
  107. package/docs/classes/TweetEntities.html +0 -128
  108. package/docs/classes/TweetFilter.html +0 -204
  109. package/docs/classes/TweetListArgs.html +0 -118
  110. package/docs/classes/TweetService.html +0 -313
  111. package/docs/classes/User.html +0 -230
  112. package/docs/classes/UserListArgs.html +0 -118
  113. package/docs/classes/UserService.html +0 -315
  114. package/docs/enums/HttpMethods.html +0 -74
  115. package/docs/functions/Rettiwt.html +0 -99
  116. package/docs/index.html +0 -161
  117. package/docs/interfaces/IAuthCookie.html +0 -104
  118. package/docs/interfaces/ICursor.html +0 -77
  119. package/docs/interfaces/ICursoredData.html +0 -93
  120. package/docs/interfaces/IDataContext.html +0 -91
  121. package/docs/interfaces/IListArgs.html +0 -87
  122. package/docs/interfaces/ITweet.html +0 -176
  123. package/docs/interfaces/ITweetEntities.html +0 -104
  124. package/docs/interfaces/ITweetFilter.html +0 -158
  125. package/docs/interfaces/IUser.html +0 -194
  126. package/docs/modules.html +0 -109
@@ -1 +1 @@
1
- {"version":3,"file":"User.js","sourceRoot":"","sources":["../../../../src/types/raw/User/User.ts"],"names":[],"mappings":""}
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.5",
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 TweetResolver(context).resolveTweets({ fromUsers: [parent.userName] } as TweetFilter, args.all ? parent.statusesCount : args.count, args.cursor)
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 flow
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(email: string): Promise<void> {
97
- // Executing the flow
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(userName: string): Promise<void> {
120
- // Executing the flow
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(password: string): Promise<void> {
143
- // Executing the flow
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 flow
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
- // Executing all login flows
249
- await this.executeLoginFlows(email, userName, password);
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
- * If cookies have been provided, then authenticated requests are made. Else, guest requests are made.
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
- 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`
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
- 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`;
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 users followed bu target user.
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
- * Twitter has a ver odd behaviour here.
26
- * If no cursor is provided, the number of followings fetched is slightly more the given count.
27
- * If a cursor if provided, the number of followings is sometimes less than the provided count.
28
- * NO SOLUTION EXISTS AS OF NOW!
29
- */
30
- 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(cursor)}%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`;
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
- * Twitter has a very odd behaviour here.
42
- * If no cursor is provided, the number of followers fetched is equal to count + 20.
43
- * If a cursor is provided, the number of followers fetched is equal to count.
44
- * The solution is to check accordingly, if a cursor if provided or not and manipulate the count
45
- */
46
- // If no cursor if provided
47
- if (!cursor) {
48
- count = count - 20;
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
- 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(cursor)}%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`;
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
- 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(cursor)}%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`;
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
+ }