rettiwt-api 6.3.0-alpha.0 → 7.0.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.
Files changed (130) hide show
  1. package/README.md +81 -31
  2. package/dist/Rettiwt.d.ts +6 -2
  3. package/dist/Rettiwt.js +7 -3
  4. package/dist/Rettiwt.js.map +1 -1
  5. package/dist/cli.js +3 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/collections/Extractors.d.ts +15 -2
  8. package/dist/collections/Extractors.js +12 -1
  9. package/dist/collections/Extractors.js.map +1 -1
  10. package/dist/collections/Groups.js +8 -0
  11. package/dist/collections/Groups.js.map +1 -1
  12. package/dist/collections/Requests.js +8 -0
  13. package/dist/collections/Requests.js.map +1 -1
  14. package/dist/commands/Space.d.ts +10 -0
  15. package/dist/commands/Space.js +38 -0
  16. package/dist/commands/Space.js.map +1 -0
  17. package/dist/commands/User.js +139 -0
  18. package/dist/commands/User.js.map +1 -1
  19. package/dist/enums/Resource.d.ts +8 -1
  20. package/dist/enums/Resource.js +8 -0
  21. package/dist/enums/Resource.js.map +1 -1
  22. package/dist/index.d.ts +11 -1
  23. package/dist/index.js +6 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/models/RettiwtConfig.d.ts +26 -3
  26. package/dist/models/RettiwtConfig.js +68 -3
  27. package/dist/models/RettiwtConfig.js.map +1 -1
  28. package/dist/models/args/FetchArgs.d.ts +3 -0
  29. package/dist/models/args/FetchArgs.js +6 -0
  30. package/dist/models/args/FetchArgs.js.map +1 -1
  31. package/dist/models/args/PostArgs.d.ts +24 -1
  32. package/dist/models/args/PostArgs.js +52 -1
  33. package/dist/models/args/PostArgs.js.map +1 -1
  34. package/dist/models/data/Space.d.ts +70 -0
  35. package/dist/models/data/Space.js +177 -0
  36. package/dist/models/data/Space.js.map +1 -0
  37. package/dist/models/data/UserAbout.d.ts +44 -0
  38. package/dist/models/data/UserAbout.js +129 -0
  39. package/dist/models/data/UserAbout.js.map +1 -0
  40. package/dist/requests/Space.d.ts +15 -0
  41. package/dist/requests/Space.js +74 -0
  42. package/dist/requests/Space.js.map +1 -0
  43. package/dist/requests/Tweet.d.ts +4 -0
  44. package/dist/requests/Tweet.js +57 -0
  45. package/dist/requests/Tweet.js.map +1 -1
  46. package/dist/requests/User.d.ts +21 -0
  47. package/dist/requests/User.js +64 -0
  48. package/dist/requests/User.js.map +1 -1
  49. package/dist/services/internal/AuthService.d.ts +25 -0
  50. package/dist/services/internal/AuthService.js +121 -0
  51. package/dist/services/internal/AuthService.js.map +1 -1
  52. package/dist/services/public/DirectMessageService.js +3 -3
  53. package/dist/services/public/DirectMessageService.js.map +1 -1
  54. package/dist/services/public/FetcherService.d.ts +4 -3
  55. package/dist/services/public/FetcherService.js +22 -16
  56. package/dist/services/public/FetcherService.js.map +1 -1
  57. package/dist/services/public/ListService.js +5 -5
  58. package/dist/services/public/ListService.js.map +1 -1
  59. package/dist/services/public/SpaceService.d.ts +42 -0
  60. package/dist/services/public/SpaceService.js +60 -0
  61. package/dist/services/public/SpaceService.js.map +1 -0
  62. package/dist/services/public/TweetService.js +26 -23
  63. package/dist/services/public/TweetService.js.map +1 -1
  64. package/dist/services/public/UserService.d.ts +79 -0
  65. package/dist/services/public/UserService.js +203 -23
  66. package/dist/services/public/UserService.js.map +1 -1
  67. package/dist/types/RettiwtConfig.d.ts +33 -3
  68. package/dist/types/args/FetchArgs.d.ts +35 -1
  69. package/dist/types/args/PostArgs.d.ts +44 -1
  70. package/dist/types/data/Space.d.ts +89 -0
  71. package/dist/types/data/Space.js +3 -0
  72. package/dist/types/data/Space.js.map +1 -0
  73. package/dist/types/data/UserAbout.d.ts +68 -0
  74. package/dist/types/data/UserAbout.js +3 -0
  75. package/dist/types/data/UserAbout.js.map +1 -0
  76. package/dist/types/raw/base/Space.d.ts +43 -22
  77. package/dist/types/raw/space/AudioSpaceById.d.ts +50 -0
  78. package/dist/types/raw/space/AudioSpaceById.js +4 -0
  79. package/dist/types/raw/space/AudioSpaceById.js.map +1 -0
  80. package/dist/types/raw/space/Details.d.ts +2 -309
  81. package/dist/types/raw/tweet/Post.d.ts +16 -1
  82. package/dist/types/raw/user/About.d.ts +65 -0
  83. package/dist/types/raw/user/About.js +4 -0
  84. package/dist/types/raw/user/About.js.map +1 -0
  85. package/dist/types/raw/user/ChangePassword.d.ts +8 -0
  86. package/dist/types/raw/user/ChangePassword.js +3 -0
  87. package/dist/types/raw/user/ChangePassword.js.map +1 -0
  88. package/dist/types/raw/user/ProfileUpdate.d.ts +1 -0
  89. package/dist/types/raw/user/Settings.d.ts +21 -0
  90. package/dist/types/raw/user/Settings.js +4 -0
  91. package/dist/types/raw/user/Settings.js.map +1 -0
  92. package/package.json +6 -4
  93. package/src/Rettiwt.ts +10 -3
  94. package/src/cli.ts +3 -1
  95. package/src/collections/Extractors.ts +22 -3
  96. package/src/collections/Groups.ts +8 -0
  97. package/src/collections/Requests.ts +11 -0
  98. package/src/commands/Space.ts +46 -0
  99. package/src/commands/User.ts +159 -0
  100. package/src/enums/Resource.ts +9 -0
  101. package/src/index.ts +11 -1
  102. package/src/models/RettiwtConfig.ts +81 -6
  103. package/src/models/args/FetchArgs.ts +6 -0
  104. package/src/models/args/PostArgs.ts +58 -1
  105. package/src/models/data/Space.ts +201 -0
  106. package/src/models/data/UserAbout.ts +161 -0
  107. package/src/requests/Space.ts +76 -0
  108. package/src/requests/Tweet.ts +59 -0
  109. package/src/requests/User.ts +69 -0
  110. package/src/services/internal/AuthService.ts +149 -1
  111. package/src/services/public/DirectMessageService.ts +3 -3
  112. package/src/services/public/FetcherService.ts +25 -18
  113. package/src/services/public/ListService.ts +5 -5
  114. package/src/services/public/SpaceService.ts +65 -0
  115. package/src/services/public/TweetService.ts +27 -24
  116. package/src/services/public/UserService.ts +247 -23
  117. package/src/types/RettiwtConfig.ts +35 -3
  118. package/src/types/args/FetchArgs.ts +41 -1
  119. package/src/types/args/PostArgs.ts +50 -1
  120. package/src/types/data/Space.ts +122 -0
  121. package/src/types/data/UserAbout.ts +87 -0
  122. package/src/types/raw/base/Space.ts +42 -22
  123. package/src/types/raw/space/AudioSpaceById.ts +57 -0
  124. package/src/types/raw/space/Details.ts +3 -352
  125. package/src/types/raw/tweet/Post.ts +19 -1
  126. package/src/types/raw/user/About.ts +77 -0
  127. package/src/types/raw/user/ChangePassword.ts +8 -0
  128. package/src/types/raw/user/ProfileUpdate.ts +1 -0
  129. package/src/types/raw/user/Settings.ts +23 -0
  130. package/tsconfig.json +2 -2
@@ -0,0 +1,76 @@
1
+ import { AxiosRequestConfig } from 'axios';
2
+
3
+ /**
4
+ * Collection of requests related to spaces.
5
+ *
6
+ * @public
7
+ */
8
+ export class SpaceRequests {
9
+ /**
10
+ * @param id - The id of the space whose details are to be fetched.
11
+ * @param withReplays - Whether to include replay information.
12
+ * @param withListeners - Whether to include listeners information.
13
+ * @param isMetatagsQuery - Whether the request is a metatags query.
14
+ */
15
+ public static details(
16
+ id: string,
17
+ withReplays?: boolean,
18
+ withListeners?: boolean,
19
+ isMetatagsQuery?: boolean,
20
+ ): AxiosRequestConfig {
21
+ return {
22
+ method: 'get',
23
+ url: 'https://x.com/i/api/graphql/rR7CQrr8kxb6fatlUaB61Q/AudioSpaceById',
24
+ params: {
25
+ /* eslint-disable @typescript-eslint/naming-convention */
26
+ variables: JSON.stringify({
27
+ id: id,
28
+ isMetatagsQuery: isMetatagsQuery ?? false,
29
+ withReplays: withReplays ?? true,
30
+ withListeners: withListeners ?? false,
31
+ }),
32
+ features: JSON.stringify({
33
+ spaces_2022_h2_spaces_communities: true,
34
+ spaces_2022_h2_clipping: true,
35
+ creator_subscriptions_tweet_preview_api_enabled: true,
36
+ profile_label_improvements_pcf_label_in_post_enabled: true,
37
+ responsive_web_profile_redirect_enabled: false,
38
+ rweb_tipjar_consumption_enabled: false,
39
+ verified_phone_label_enabled: false,
40
+ premium_content_api_read_enabled: false,
41
+ communities_web_enable_tweet_community_results_fetch: true,
42
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
43
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
44
+ responsive_web_grok_analyze_post_followups_enabled: true,
45
+ responsive_web_jetfuel_frame: true,
46
+ responsive_web_grok_share_attachment_enabled: true,
47
+ responsive_web_grok_annotations_enabled: false,
48
+ articles_preview_enabled: true,
49
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
50
+ responsive_web_edit_tweet_api_enabled: true,
51
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
52
+ view_counts_everywhere_api_enabled: true,
53
+ longform_notetweets_consumption_enabled: true,
54
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
55
+ tweet_awards_web_tipping_enabled: false,
56
+ responsive_web_grok_show_grok_translated_post: false,
57
+ responsive_web_grok_analysis_button_from_backend: true,
58
+ post_ctas_fetch_enabled: true,
59
+ creator_subscriptions_quote_tweet_preview_enabled: false,
60
+ freedom_of_speech_not_reach_fetch_enabled: true,
61
+ standardized_nudges_misinfo: true,
62
+ tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
63
+ longform_notetweets_rich_text_read_enabled: true,
64
+ longform_notetweets_inline_media_enabled: true,
65
+ responsive_web_grok_image_annotation_enabled: true,
66
+ responsive_web_grok_imagine_annotation_enabled: true,
67
+ responsive_web_graphql_timeline_navigation_enabled: true,
68
+ responsive_web_grok_community_note_auto_translation_is_enabled: false,
69
+ responsive_web_enhance_cards_enabled: false,
70
+ }),
71
+ /* eslint-enable @typescript-eslint/naming-convention */
72
+ },
73
+ paramsSerializer: { encode: encodeURIComponent },
74
+ };
75
+ }
76
+ }
@@ -287,6 +287,65 @@ export class TweetRequests {
287
287
  };
288
288
  }
289
289
 
290
+ /**
291
+ * @param args - The configuration object for the long-form tweet to be posted (X Premium only).
292
+ */
293
+ public static postNote(args: INewTweet): AxiosRequestConfig {
294
+ // Parsing the args
295
+ const parsedArgs = new NewTweet(args);
296
+
297
+ return {
298
+ method: 'post',
299
+ url: 'https://x.com/i/api/graphql/_eeuQKX1-VyRP_ROM-GN7g/CreateNoteTweet',
300
+ data: {
301
+ /* eslint-disable @typescript-eslint/naming-convention */
302
+ variables: {
303
+ tweet_text: parsedArgs.text,
304
+ media: parsedArgs.media ? new MediaVariable(parsedArgs.media) : undefined,
305
+ semantic_annotation_ids: [],
306
+ disallowed_reply_options: null,
307
+ },
308
+ features: {
309
+ premium_content_api_read_enabled: false,
310
+ communities_web_enable_tweet_community_results_fetch: true,
311
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
312
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
313
+ responsive_web_grok_analyze_post_followups_enabled: true,
314
+ responsive_web_jetfuel_frame: true,
315
+ responsive_web_grok_share_attachment_enabled: true,
316
+ responsive_web_grok_annotations_enabled: true,
317
+ responsive_web_edit_tweet_api_enabled: true,
318
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
319
+ view_counts_everywhere_api_enabled: true,
320
+ longform_notetweets_consumption_enabled: true,
321
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
322
+ tweet_awards_web_tipping_enabled: false,
323
+ content_disclosure_indicator_enabled: true,
324
+ content_disclosure_ai_generated_indicator_enabled: true,
325
+ responsive_web_grok_show_grok_translated_post: true,
326
+ responsive_web_grok_analysis_button_from_backend: true,
327
+ post_ctas_fetch_enabled: true,
328
+ longform_notetweets_rich_text_read_enabled: true,
329
+ longform_notetweets_inline_media_enabled: false,
330
+ profile_label_improvements_pcf_label_in_post_enabled: true,
331
+ responsive_web_profile_redirect_enabled: false,
332
+ rweb_tipjar_consumption_enabled: false,
333
+ verified_phone_label_enabled: false,
334
+ articles_preview_enabled: true,
335
+ responsive_web_grok_community_note_auto_translation_is_enabled: false,
336
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
337
+ freedom_of_speech_not_reach_fetch_enabled: true,
338
+ standardized_nudges_misinfo: true,
339
+ tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
340
+ responsive_web_grok_image_annotation_enabled: true,
341
+ responsive_web_grok_imagine_annotation_enabled: true,
342
+ responsive_web_graphql_timeline_navigation_enabled: true,
343
+ responsive_web_enhance_cards_enabled: false,
344
+ },
345
+ },
346
+ };
347
+ }
348
+
290
349
  /**
291
350
  * @param id - The id of the tweet whose replies are to be fetched.
292
351
  * @param cursor - The cursor to the batch of replies to fetch.
@@ -11,6 +11,20 @@ import { IProfileUpdateOptions } from '../types/args/ProfileArgs';
11
11
  * @public
12
12
  */
13
13
  export class UserRequests {
14
+ /**
15
+ * @param userName - The username of the user whose about profile is to be fetched.
16
+ */
17
+ public static aboutByUsername(userName: string): AxiosRequestConfig {
18
+ return {
19
+ method: 'get',
20
+ url: 'https://x.com/i/api/graphql/zs_jFPFT78rBpXv9Z3U2YQ/AboutAccountQuery',
21
+ params: {
22
+ variables: JSON.stringify({ screenName: userName }),
23
+ },
24
+ paramsSerializer: { encode: encodeURIComponent },
25
+ };
26
+ }
27
+
14
28
  /**
15
29
  * @param id - The id of the user whose affiliates are to be fetched.
16
30
  * @param count - The number of affiliates to fetch. Only works as a lower limit when used with a cursor.
@@ -311,6 +325,39 @@ export class UserRequests {
311
325
  };
312
326
  }
313
327
 
328
+ /**
329
+ * @param currentPassword - The current password.
330
+ * @param newPassword - The new password.
331
+ */
332
+ public static changePassword(currentPassword: string, newPassword: string): AxiosRequestConfig {
333
+ return {
334
+ method: 'post',
335
+ url: 'https://x.com/i/api/i/account/change_password.json',
336
+ data: qs.stringify({
337
+ /* eslint-disable @typescript-eslint/naming-convention */
338
+ current_password: currentPassword,
339
+ password: newPassword,
340
+ password_confirmation: newPassword,
341
+ /* eslint-enable @typescript-eslint/naming-convention */
342
+ }),
343
+ };
344
+ }
345
+
346
+ /**
347
+ * @param newUsername - The new username to set.
348
+ */
349
+ public static changeUsername(newUsername: string): AxiosRequestConfig {
350
+ return {
351
+ method: 'post',
352
+ url: 'https://x.com/i/api/1.1/account/settings.json',
353
+ data: qs.stringify({
354
+ /* eslint-disable @typescript-eslint/naming-convention */
355
+ screen_name: newUsername,
356
+ /* eslint-enable @typescript-eslint/naming-convention */
357
+ }),
358
+ };
359
+ }
360
+
314
361
  /**
315
362
  * @param id - The id of the user whose details are to be fetched.
316
363
  */
@@ -1209,4 +1256,26 @@ export class UserRequests {
1209
1256
  }),
1210
1257
  };
1211
1258
  }
1259
+
1260
+ /**
1261
+ * @param bannerBase64 - The base64-encoded banner image data.
1262
+ */
1263
+ public static updateProfileBanner(bannerBase64: string): AxiosRequestConfig {
1264
+ return {
1265
+ method: 'post',
1266
+ url: 'https://x.com/i/api/1.1/account/update_profile_banner.json',
1267
+ data: qs.stringify({ banner: bannerBase64 }),
1268
+ };
1269
+ }
1270
+
1271
+ /**
1272
+ * @param imageBase64 - The base64-encoded image data.
1273
+ */
1274
+ public static updateProfileImage(imageBase64: string): AxiosRequestConfig {
1275
+ return {
1276
+ method: 'post',
1277
+ url: 'https://x.com/i/api/1.1/account/update_profile_image.json',
1278
+ data: qs.stringify({ image: imageBase64 }),
1279
+ };
1280
+ }
1212
1281
  }
@@ -1,4 +1,6 @@
1
- import axios from 'axios';
1
+ import axios, { AxiosResponse } from 'axios';
2
+
3
+ import { Cookie } from 'cookiejar';
2
4
 
3
5
  import { ApiErrors } from '../../enums/Api';
4
6
  import { AuthCredential } from '../../models/auth/AuthCredential';
@@ -20,6 +22,21 @@ export class AuthService {
20
22
  this._config = config;
21
23
  }
22
24
 
25
+ /**
26
+ * Splits the cookie header into a list of key=value pairs.
27
+ *
28
+ * @param cookieHeader - The value of the cookie header.
29
+ *
30
+ * @returns The list of key=value pairs in the cookies.
31
+ */
32
+ private static _splitCookieHeader(cookieHeader: string | string[]): string[] {
33
+ if (Array.isArray(cookieHeader)) {
34
+ return cookieHeader;
35
+ }
36
+
37
+ return cookieHeader.split(/,(?=\s*[^;,]+=)/g);
38
+ }
39
+
23
40
  /**
24
41
  * Decodes the encoded cookie string.
25
42
  *
@@ -46,6 +63,91 @@ export class AuthService {
46
63
  return encodedCookies;
47
64
  }
48
65
 
66
+ /**
67
+ * Gets a new API key from an HTTP response.
68
+ *
69
+ * @param response - The HTTP response received.
70
+ * @param config - The current Rettiwt config.
71
+ *
72
+ * @returns The new API key.
73
+ */
74
+ public static getApiKeyFromReponse(response: AxiosResponse, config?: RettiwtConfig): string | undefined {
75
+ // If new cookies not returned or user not authenticated, terminate
76
+ if (response.headers['set-cookie'] === undefined || config?.apiKey === undefined) {
77
+ return;
78
+ }
79
+
80
+ /** The collection of required cookie names. */
81
+ const requiredCookieNames = new Set(['auth_token', 'ct0', 'kdt', 'twid']);
82
+
83
+ /** The current cookie string. */
84
+ const currentCookieString = AuthService.decodeCookie(config.apiKey);
85
+
86
+ /** The cookie key=value pairs from the response. */
87
+ const cookies = AuthService._splitCookieHeader(response.headers['set-cookie']);
88
+
89
+ /** The map from cookie key to value. */
90
+ const cookiesMap = new Map<string, string>();
91
+
92
+ for (const cookieEntry of currentCookieString.split(';')) {
93
+ const trimmedEntry = cookieEntry.trim();
94
+ const separatorIndex = trimmedEntry.indexOf('=');
95
+
96
+ if (!trimmedEntry || separatorIndex < 1) {
97
+ continue;
98
+ }
99
+
100
+ const key = trimmedEntry.slice(0, separatorIndex).trim();
101
+ const value = trimmedEntry.slice(separatorIndex + 1).trim();
102
+ if (!key || !value || !requiredCookieNames.has(key)) {
103
+ continue;
104
+ }
105
+
106
+ cookiesMap.set(key, value);
107
+ }
108
+
109
+ let hasUpdate = false;
110
+ for (const cookie of cookies) {
111
+ const cookieValuePair = cookie.split(';', 1)[0]?.trim();
112
+ const separatorIndex = cookieValuePair?.indexOf('=') ?? -1;
113
+
114
+ if (!cookieValuePair || separatorIndex < 1) {
115
+ continue;
116
+ }
117
+
118
+ const key = cookieValuePair.slice(0, separatorIndex).trim();
119
+ const value = cookieValuePair.slice(separatorIndex + 1).trim();
120
+ if (!key || !value || !requiredCookieNames.has(key)) {
121
+ continue;
122
+ }
123
+
124
+ cookiesMap.set(key, value);
125
+ hasUpdate = true;
126
+ }
127
+
128
+ if (!hasUpdate || !cookiesMap.has('twid')) {
129
+ return;
130
+ }
131
+
132
+ let mergedCookieString = '';
133
+ for (const [key, value] of cookiesMap.entries()) {
134
+ mergedCookieString += `${key}=${value};`;
135
+ }
136
+
137
+ if (!mergedCookieString) {
138
+ return;
139
+ }
140
+
141
+ try {
142
+ // Encoding the new cookies into an API key
143
+ const newApiKey = AuthService.encodeCookie(mergedCookieString);
144
+
145
+ return newApiKey;
146
+ } catch {
147
+ return;
148
+ }
149
+ }
150
+
49
151
  /**
50
152
  * Gets the user's id from the given API key.
51
153
  *
@@ -71,6 +173,50 @@ export class AuthService {
71
173
  }
72
174
  }
73
175
 
176
+ /**
177
+ * Fetches a fresh CSRF from Twitter by making a lightweight
178
+ * authenticated request, then rotates the apiKey with the updated cookie.
179
+ *
180
+ * @param config - The current Rettiwt config.
181
+ */
182
+ public static async refreshCsrfToken(config: RettiwtConfig): Promise<void> {
183
+ // If unauthenticated, skip
184
+ if (config.apiKey === undefined) {
185
+ return;
186
+ }
187
+
188
+ try {
189
+ const cred = new AuthCredential(
190
+ AuthService.decodeCookie(config.apiKey)
191
+ .split(';')
192
+ .map((item) => new Cookie(item)),
193
+ );
194
+
195
+ const refreshResponse = await axios.get('https://x.com/i/api/1.1/account/verify_credentials.json', {
196
+ headers: {
197
+ ...cred.toHeader(),
198
+ authorization:
199
+ 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
200
+ },
201
+ httpAgent: config.httpAgent,
202
+ httpsAgent: config.httpsAgent,
203
+ proxy: config.axiosProxyConfig,
204
+ validateStatus: () => true,
205
+ });
206
+
207
+ // Getting the new API key
208
+ const newApiKey = AuthService.getApiKeyFromReponse(refreshResponse, config);
209
+
210
+ // If new API key is generated, update current API key
211
+ if (newApiKey !== undefined) {
212
+ config.apiKey = newApiKey;
213
+ }
214
+ } catch {
215
+ // If ct0 refresh fails, leave apiKey as-is
216
+ return;
217
+ }
218
+ }
219
+
74
220
  /**
75
221
  * Login to twitter as guest.
76
222
  *
@@ -106,7 +252,9 @@ export class AuthService {
106
252
  /* eslint-enable @typescript-eslint/naming-convention */
107
253
  }>('https://api.twitter.com/1.1/guest/activate.json', undefined, {
108
254
  headers: cred.toHeader(),
255
+ httpAgent: this._config.httpAgent,
109
256
  httpsAgent: this._config.httpsAgent,
257
+ proxy: this._config.axiosProxyConfig,
110
258
  })
111
259
  .then((res) => {
112
260
  cred.guestToken = res.data.guest_token;
@@ -64,7 +64,7 @@ export class DirectMessageService extends FetcherService {
64
64
  });
65
65
 
66
66
  // Deserializing response
67
- const data = Extractors[resource](response);
67
+ const data = Extractors[resource](response.data);
68
68
 
69
69
  return data;
70
70
  }
@@ -139,7 +139,7 @@ export class DirectMessageService extends FetcherService {
139
139
  });
140
140
 
141
141
  // Deserializing response
142
- const data = Extractors[resource](response);
142
+ const data = Extractors[resource](response.data);
143
143
 
144
144
  return data;
145
145
  }
@@ -151,7 +151,7 @@ export class DirectMessageService extends FetcherService {
151
151
  const response = await this.request<IInboxInitialResponse>(resource, {});
152
152
 
153
153
  // Deserializing response
154
- const data = Extractors[resource](response);
154
+ const data = Extractors[resource](response.data);
155
155
 
156
156
  return data;
157
157
  }
@@ -1,6 +1,6 @@
1
- import axios, { AxiosError, isAxiosError } from 'axios';
1
+ import axios, { AxiosError, AxiosResponse, isAxiosError } from 'axios';
2
2
  import { Cookie } from 'cookiejar';
3
- import { JSDOM } from 'jsdom';
3
+ import { parseHTML } from 'linkedom';
4
4
  import { ClientTransaction } from 'x-client-transaction-id';
5
5
 
6
6
  import { AllowGuestAuthenticationGroup, FetchResourcesGroup, PostResourcesGroup } from '../../collections/Groups';
@@ -128,13 +128,13 @@ export class FetcherService {
128
128
  // Fetch X.com homepage
129
129
  const homePageResponse = await axios.get<string>('https://x.com', {
130
130
  headers: this.config.headers,
131
- httpAgent: this.config.httpsAgent,
131
+ httpAgent: this.config.httpAgent,
132
132
  httpsAgent: this.config.httpsAgent,
133
+ proxy: this.config.axiosProxyConfig,
133
134
  });
134
135
 
135
136
  // Parse HTML using linkedom
136
- let dom = new JSDOM(homePageResponse.data);
137
- let document = dom.window.document;
137
+ let document = parseHTML(homePageResponse.data).document;
138
138
 
139
139
  // Check for migration redirection links
140
140
  const migrationRedirectionRegex = new RegExp(
@@ -151,12 +151,12 @@ export class FetcherService {
151
151
  if (migrationRedirectionUrl) {
152
152
  // Follow redirection URL
153
153
  const redirectResponse = await axios.get<string>(migrationRedirectionUrl[0], {
154
- httpAgent: this.config.httpsAgent,
154
+ httpAgent: this.config.httpAgent,
155
155
  httpsAgent: this.config.httpsAgent,
156
+ proxy: this.config.axiosProxyConfig,
156
157
  });
157
158
 
158
- dom = new JSDOM(redirectResponse.data);
159
- document = dom.window.document;
159
+ document = parseHTML(redirectResponse.data).document;
160
160
  }
161
161
 
162
162
  // Handle migration form if present
@@ -193,12 +193,12 @@ export class FetcherService {
193
193
 
194
194
  /* eslint-enable @typescript-eslint/naming-convention */
195
195
  },
196
- httpAgent: this.config.httpsAgent,
196
+ httpAgent: this.config.httpAgent,
197
197
  httpsAgent: this.config.httpsAgent,
198
+ proxy: this.config.axiosProxyConfig,
198
199
  });
199
200
 
200
- dom = new JSDOM(formResponse.data);
201
- document = dom.window.document;
201
+ document = parseHTML(formResponse.data).document;
202
202
  }
203
203
 
204
204
  // Return final DOM document
@@ -254,11 +254,11 @@ export class FetcherService {
254
254
  * Makes an HTTP request according to the given parameters.
255
255
  *
256
256
  * @param resource - The requested resource.
257
- * @param config - The request configuration.
257
+ * @param args - The args to be used for the request.
258
258
  *
259
259
  * @typeParam T - The type of the returned response data.
260
260
  *
261
- * @returns The raw data response received.
261
+ * @returns The raw Axios response.
262
262
  *
263
263
  * @example
264
264
  *
@@ -279,7 +279,7 @@ export class FetcherService {
279
279
  * });
280
280
  * ```
281
281
  */
282
- public async request<T = unknown>(resource: ResourceType, args: IFetchArgs | IPostArgs): Promise<T> {
282
+ public async request<T = unknown>(resource: ResourceType, args: IFetchArgs | IPostArgs): Promise<AxiosResponse<T>> {
283
283
  /** The current retry number. */
284
284
  let retry = 0;
285
285
 
@@ -307,8 +307,9 @@ export class FetcherService {
307
307
  ...cred.toHeader(),
308
308
  ...this.config.headers,
309
309
  };
310
- config.httpAgent = this.config.httpsAgent;
310
+ config.httpAgent = this.config.httpAgent;
311
311
  config.httpsAgent = this.config.httpsAgent;
312
+ config.proxy = this.config.axiosProxyConfig;
312
313
  config.timeout = this._timeout;
313
314
 
314
315
  // Using retries for error 404
@@ -325,7 +326,8 @@ export class FetcherService {
325
326
  await this._wait();
326
327
 
327
328
  // Getting the response body
328
- const responseData = (await axios<T>(config)).data;
329
+ const response = await axios<T>(config);
330
+ const responseData = response.data;
329
331
 
330
332
  // Check for Twitter API errors in response body
331
333
  // Type guard to check if response contains errors
@@ -348,8 +350,13 @@ export class FetcherService {
348
350
  throw new TwitterError(axiosError);
349
351
  }
350
352
 
351
- // Returning the reponse body
352
- return responseData;
353
+ // Calling the request middleware, if configured
354
+ if (this.config.responseMiddleware !== undefined) {
355
+ await this.config.responseMiddleware(response);
356
+ }
357
+
358
+ // Returning the response
359
+ return response;
353
360
  } catch (err) {
354
361
  // If it's an error 404, retry
355
362
  if (isAxiosError(err) && err.status === 404) {
@@ -59,7 +59,7 @@ export class ListService extends FetcherService {
59
59
  });
60
60
 
61
61
  // Deserializing response
62
- const data = Extractors[resource](response);
62
+ const data = Extractors[resource](response.data);
63
63
 
64
64
  return data;
65
65
  }
@@ -100,7 +100,7 @@ export class ListService extends FetcherService {
100
100
  const response = await this.request<IListDetailsResponse>(resource, { id: id });
101
101
 
102
102
  // Deserializing response
103
- const data = Extractors[resource](response, id);
103
+ const data = Extractors[resource](response.data, id);
104
104
 
105
105
  return data;
106
106
  }
@@ -145,7 +145,7 @@ export class ListService extends FetcherService {
145
145
  });
146
146
 
147
147
  // Deserializing response
148
- const data = Extractors[resource](response);
148
+ const data = Extractors[resource](response.data);
149
149
 
150
150
  return data;
151
151
  }
@@ -186,7 +186,7 @@ export class ListService extends FetcherService {
186
186
  });
187
187
 
188
188
  // Deserializing response
189
- const data = Extractors[resource](response);
189
+ const data = Extractors[resource](response.data);
190
190
 
191
191
  return data;
192
192
  }
@@ -231,7 +231,7 @@ export class ListService extends FetcherService {
231
231
  });
232
232
 
233
233
  // Deserializing response
234
- const data = Extractors[resource](response);
234
+ const data = Extractors[resource](response.data);
235
235
 
236
236
  // Sorting the tweets by date, from recent to oldest
237
237
  data.list.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
@@ -0,0 +1,65 @@
1
+ import { Extractors } from '../../collections/Extractors';
2
+ import { ResourceType } from '../../enums/Resource';
3
+ import { Space } from '../../models/data/Space';
4
+ import { RettiwtConfig } from '../../models/RettiwtConfig';
5
+ import { ISpaceDetailsOptions } from '../../types/args/FetchArgs';
6
+ import { IAudioSpaceByIdResponse } from '../../types/raw/space/AudioSpaceById';
7
+
8
+ import { FetcherService } from './FetcherService';
9
+
10
+ /**
11
+ * Handles interacting with resources related to spaces.
12
+ *
13
+ * @public
14
+ */
15
+ export class SpaceService extends FetcherService {
16
+ /**
17
+ * @param config - The config object for configuring the Rettiwt instance.
18
+ *
19
+ * @internal
20
+ */
21
+ public constructor(config: RettiwtConfig) {
22
+ super(config);
23
+ }
24
+
25
+ /**
26
+ * Get the details of a space.
27
+ *
28
+ * @param id - The ID of the target space.
29
+ * @param options - Additional options for the fetch.
30
+ *
31
+ * @returns The details of the space with the given ID.
32
+ *
33
+ * @example
34
+ *
35
+ * ```ts
36
+ * import { Rettiwt } from 'rettiwt-api';
37
+ *
38
+ * const rettiwt = new Rettiwt({ apiKey: API_KEY });
39
+ *
40
+ * rettiwt.space.details('1YqJDNEzvoVKV', { withListeners: true })
41
+ * .then(res => {
42
+ * console.log(res);
43
+ * })
44
+ * .catch(err => {
45
+ * console.log(err);
46
+ * });
47
+ * ```
48
+ */
49
+ public async details(id: string, options?: ISpaceDetailsOptions): Promise<Space | undefined> {
50
+ const resource = ResourceType.SPACE_DETAILS;
51
+
52
+ // Fetching raw space details
53
+ const response = await this.request<IAudioSpaceByIdResponse>(resource, {
54
+ id: id,
55
+ withReplays: options?.withReplays,
56
+ withListeners: options?.withListeners,
57
+ isMetatagsQuery: options?.isMetatagsQuery,
58
+ });
59
+
60
+ // Deserializing response
61
+ const data = Extractors[resource](response.data);
62
+
63
+ return data;
64
+ }
65
+ }