rettiwt-api 6.3.0-alpha.1 → 7.0.1

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 (138) 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/User.d.ts +1 -1
  38. package/dist/models/data/User.js +3 -3
  39. package/dist/models/data/User.js.map +1 -1
  40. package/dist/models/data/UserAbout.d.ts +44 -0
  41. package/dist/models/data/UserAbout.js +129 -0
  42. package/dist/models/data/UserAbout.js.map +1 -0
  43. package/dist/requests/Space.d.ts +15 -0
  44. package/dist/requests/Space.js +74 -0
  45. package/dist/requests/Space.js.map +1 -0
  46. package/dist/requests/Tweet.d.ts +4 -0
  47. package/dist/requests/Tweet.js +57 -0
  48. package/dist/requests/Tweet.js.map +1 -1
  49. package/dist/requests/User.d.ts +21 -0
  50. package/dist/requests/User.js +64 -0
  51. package/dist/requests/User.js.map +1 -1
  52. package/dist/services/internal/AuthService.d.ts +25 -0
  53. package/dist/services/internal/AuthService.js +121 -0
  54. package/dist/services/internal/AuthService.js.map +1 -1
  55. package/dist/services/public/DirectMessageService.js +3 -3
  56. package/dist/services/public/DirectMessageService.js.map +1 -1
  57. package/dist/services/public/FetcherService.d.ts +4 -3
  58. package/dist/services/public/FetcherService.js +22 -16
  59. package/dist/services/public/FetcherService.js.map +1 -1
  60. package/dist/services/public/ListService.js +5 -5
  61. package/dist/services/public/ListService.js.map +1 -1
  62. package/dist/services/public/SpaceService.d.ts +42 -0
  63. package/dist/services/public/SpaceService.js +60 -0
  64. package/dist/services/public/SpaceService.js.map +1 -0
  65. package/dist/services/public/TweetService.js +26 -23
  66. package/dist/services/public/TweetService.js.map +1 -1
  67. package/dist/services/public/UserService.d.ts +79 -0
  68. package/dist/services/public/UserService.js +203 -23
  69. package/dist/services/public/UserService.js.map +1 -1
  70. package/dist/types/RettiwtConfig.d.ts +33 -3
  71. package/dist/types/args/FetchArgs.d.ts +35 -1
  72. package/dist/types/args/PostArgs.d.ts +44 -1
  73. package/dist/types/data/Space.d.ts +89 -0
  74. package/dist/types/data/Space.js +3 -0
  75. package/dist/types/data/Space.js.map +1 -0
  76. package/dist/types/data/User.d.ts +2 -2
  77. package/dist/types/data/UserAbout.d.ts +68 -0
  78. package/dist/types/data/UserAbout.js +3 -0
  79. package/dist/types/data/UserAbout.js.map +1 -0
  80. package/dist/types/raw/base/Space.d.ts +43 -22
  81. package/dist/types/raw/base/User.d.ts +1 -1
  82. package/dist/types/raw/space/AudioSpaceById.d.ts +50 -0
  83. package/dist/types/raw/space/AudioSpaceById.js +4 -0
  84. package/dist/types/raw/space/AudioSpaceById.js.map +1 -0
  85. package/dist/types/raw/space/Details.d.ts +2 -309
  86. package/dist/types/raw/tweet/Post.d.ts +16 -1
  87. package/dist/types/raw/user/About.d.ts +65 -0
  88. package/dist/types/raw/user/About.js +4 -0
  89. package/dist/types/raw/user/About.js.map +1 -0
  90. package/dist/types/raw/user/ChangePassword.d.ts +8 -0
  91. package/dist/types/raw/user/ChangePassword.js +3 -0
  92. package/dist/types/raw/user/ChangePassword.js.map +1 -0
  93. package/dist/types/raw/user/ProfileUpdate.d.ts +1 -0
  94. package/dist/types/raw/user/Settings.d.ts +21 -0
  95. package/dist/types/raw/user/Settings.js +4 -0
  96. package/dist/types/raw/user/Settings.js.map +1 -0
  97. package/package.json +5 -3
  98. package/src/Rettiwt.ts +10 -3
  99. package/src/cli.ts +3 -1
  100. package/src/collections/Extractors.ts +22 -3
  101. package/src/collections/Groups.ts +8 -0
  102. package/src/collections/Requests.ts +11 -0
  103. package/src/commands/Space.ts +46 -0
  104. package/src/commands/User.ts +159 -0
  105. package/src/enums/Resource.ts +9 -0
  106. package/src/index.ts +11 -1
  107. package/src/models/RettiwtConfig.ts +81 -6
  108. package/src/models/args/FetchArgs.ts +6 -0
  109. package/src/models/args/PostArgs.ts +58 -1
  110. package/src/models/data/Space.ts +201 -0
  111. package/src/models/data/User.ts +3 -3
  112. package/src/models/data/UserAbout.ts +161 -0
  113. package/src/requests/Space.ts +76 -0
  114. package/src/requests/Tweet.ts +59 -0
  115. package/src/requests/User.ts +69 -0
  116. package/src/services/internal/AuthService.ts +149 -1
  117. package/src/services/public/DirectMessageService.ts +3 -3
  118. package/src/services/public/FetcherService.ts +25 -18
  119. package/src/services/public/ListService.ts +5 -5
  120. package/src/services/public/SpaceService.ts +65 -0
  121. package/src/services/public/TweetService.ts +27 -24
  122. package/src/services/public/UserService.ts +247 -23
  123. package/src/types/RettiwtConfig.ts +35 -3
  124. package/src/types/args/FetchArgs.ts +41 -1
  125. package/src/types/args/PostArgs.ts +50 -1
  126. package/src/types/data/Space.ts +122 -0
  127. package/src/types/data/User.ts +2 -2
  128. package/src/types/data/UserAbout.ts +87 -0
  129. package/src/types/raw/base/Space.ts +42 -22
  130. package/src/types/raw/base/User.ts +1 -1
  131. package/src/types/raw/space/AudioSpaceById.ts +57 -0
  132. package/src/types/raw/space/Details.ts +3 -352
  133. package/src/types/raw/tweet/Post.ts +19 -1
  134. package/src/types/raw/user/About.ts +77 -0
  135. package/src/types/raw/user/ChangePassword.ts +8 -0
  136. package/src/types/raw/user/ProfileUpdate.ts +1 -0
  137. package/src/types/raw/user/Settings.ts +23 -0
  138. package/tsconfig.json +2 -2
@@ -0,0 +1,161 @@
1
+ import { LogActions } from '../../enums/Logging';
2
+ import { LogService } from '../../services/internal/LogService';
3
+ import {
4
+ IUserAbout,
5
+ IUserAboutProfile,
6
+ IUserAboutUsernameChanges,
7
+ IUserAboutVerificationInfo,
8
+ } from '../../types/data/UserAbout';
9
+ import { IUserAboutResponse, IUserAboutResult } from '../../types/raw/user/About';
10
+
11
+ /* eslint-disable @typescript-eslint/naming-convention */
12
+ type IRawUsernameChanges = {
13
+ count?: string;
14
+ last_changed_at_msec?: string;
15
+ };
16
+ /* eslint-enable @typescript-eslint/naming-convention */
17
+
18
+ /**
19
+ * The about profile details of a single user.
20
+ *
21
+ * @public
22
+ */
23
+ export class UserAbout implements IUserAbout {
24
+ /** The raw about profile details. */
25
+ private readonly _raw: IUserAboutResult;
26
+
27
+ public aboutProfile?: IUserAboutProfile;
28
+ public createdAt: string;
29
+ public fullName: string;
30
+ public id: string;
31
+ public isProtected?: boolean;
32
+ public isVerified: boolean;
33
+ public profileImage: string;
34
+ public profileImageShape?: string;
35
+ public userName: string;
36
+ public verificationInfo?: IUserAboutVerificationInfo;
37
+
38
+ /**
39
+ * @param user - The raw about profile details.
40
+ */
41
+ public constructor(user: IUserAboutResult) {
42
+ this._raw = { ...user };
43
+
44
+ this.id = user.rest_id ?? user.id ?? '';
45
+ this.userName = user.core?.screen_name ?? '';
46
+ this.fullName = user.core?.name ?? '';
47
+ this.createdAt = new Date(user.core?.created_at ?? 0).toISOString();
48
+ this.profileImage = user.avatar?.image_url ?? '';
49
+ this.profileImageShape = user.profile_image_shape;
50
+ this.isVerified = user.is_blue_verified ?? false;
51
+ this.isProtected = user.privacy?.protected;
52
+ this.aboutProfile = UserAbout._buildAboutProfile(user);
53
+ this.verificationInfo = UserAbout._buildVerificationInfo(user);
54
+ }
55
+
56
+ /** The raw about profile details. */
57
+ public get raw(): IUserAboutResult {
58
+ return { ...this._raw };
59
+ }
60
+
61
+ private static _buildAboutProfile(user: IUserAboutResult): IUserAboutProfile | undefined {
62
+ const profile = user.about_profile;
63
+
64
+ if (!profile) {
65
+ return undefined;
66
+ }
67
+
68
+ const usernameChanges = UserAbout._buildUsernameChanges(profile.username_changes);
69
+
70
+ return {
71
+ createdCountryAccurate: profile.created_country_accurate,
72
+ accountBasedIn: profile.account_based_in,
73
+ locationAccurate: profile.location_accurate,
74
+ learnMoreUrl: profile.learn_more_url,
75
+ source: profile.source,
76
+ usernameChanges: usernameChanges,
77
+ };
78
+ }
79
+
80
+ private static _buildUsernameChanges(changes?: IRawUsernameChanges): IUserAboutUsernameChanges | undefined {
81
+ if (!changes) {
82
+ return undefined;
83
+ }
84
+
85
+ return {
86
+ count: UserAbout._toNumber(changes.count),
87
+ lastChangedAt: UserAbout._toIsoFromMsec(changes.last_changed_at_msec),
88
+ };
89
+ }
90
+
91
+ private static _buildVerificationInfo(user: IUserAboutResult): IUserAboutVerificationInfo | undefined {
92
+ const info = user.verification_info;
93
+
94
+ if (!info) {
95
+ return undefined;
96
+ }
97
+
98
+ return {
99
+ isIdentityVerified: info.is_identity_verified,
100
+ verifiedSince: UserAbout._toIsoFromMsec(info.reason?.verified_since_msec),
101
+ };
102
+ }
103
+
104
+ private static _toIsoFromMsec(value?: string | number): string | undefined {
105
+ const parsed = UserAbout._toNumber(value);
106
+
107
+ return parsed === undefined ? undefined : new Date(parsed).toISOString();
108
+ }
109
+
110
+ private static _toNumber(value?: string | number): number | undefined {
111
+ if (value === undefined || value === null) {
112
+ return undefined;
113
+ }
114
+
115
+ const parsed = typeof value === 'number' ? value : Number(value);
116
+
117
+ return Number.isFinite(parsed) ? parsed : undefined;
118
+ }
119
+
120
+ /**
121
+ * Extracts and deserializes a single target user about profile from the given raw response data.
122
+ *
123
+ * @param response - The raw response data.
124
+ *
125
+ * @returns The target deserialized user about profile.
126
+ */
127
+ public static single(response: NonNullable<unknown>): UserAbout | undefined {
128
+ const result = (response as IUserAboutResponse)?.data?.user_result_by_screen_name?.result;
129
+
130
+ if (!result || result.__typename !== 'User') {
131
+ LogService.log(LogActions.WARNING, {
132
+ action: LogActions.DESERIALIZE,
133
+ message: `User not found, skipping`,
134
+ });
135
+ return undefined;
136
+ }
137
+
138
+ // Logging
139
+ LogService.log(LogActions.DESERIALIZE, { id: result.rest_id ?? result.id });
140
+
141
+ return new UserAbout(result);
142
+ }
143
+
144
+ /**
145
+ * @returns A serializable JSON representation of `this` object.
146
+ */
147
+ public toJSON(): IUserAbout {
148
+ return {
149
+ id: this.id,
150
+ userName: this.userName,
151
+ fullName: this.fullName,
152
+ createdAt: this.createdAt,
153
+ profileImage: this.profileImage,
154
+ profileImageShape: this.profileImageShape,
155
+ isVerified: this.isVerified,
156
+ isProtected: this.isProtected,
157
+ aboutProfile: this.aboutProfile,
158
+ verificationInfo: this.verificationInfo,
159
+ };
160
+ }
161
+ }
@@ -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
  }