@youmind-openlab/rettiwt-api 1.0.3 → 1.0.4

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 (61) hide show
  1. package/.devcontainer/devcontainer.json +20 -0
  2. package/README.md +326 -256
  3. package/dist/collections/Extractors.d.ts +9 -2
  4. package/dist/collections/Extractors.js +8 -1
  5. package/dist/collections/Extractors.js.map +1 -1
  6. package/dist/collections/Groups.js +5 -0
  7. package/dist/collections/Groups.js.map +1 -1
  8. package/dist/collections/Requests.js +5 -0
  9. package/dist/collections/Requests.js.map +1 -1
  10. package/dist/commands/User.js +126 -0
  11. package/dist/commands/User.js.map +1 -1
  12. package/dist/enums/Resource.d.ts +6 -1
  13. package/dist/enums/Resource.js +5 -0
  14. package/dist/enums/Resource.js.map +1 -1
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/models/args/PostArgs.d.ts +16 -1
  18. package/dist/models/args/PostArgs.js +44 -1
  19. package/dist/models/args/PostArgs.js.map +1 -1
  20. package/dist/requests/Tweet.d.ts +4 -0
  21. package/dist/requests/Tweet.js +57 -0
  22. package/dist/requests/Tweet.js.map +1 -1
  23. package/dist/requests/User.d.ts +25 -0
  24. package/dist/requests/User.js +59 -0
  25. package/dist/requests/User.js.map +1 -1
  26. package/dist/services/public/FetcherService.d.ts +14 -2
  27. package/dist/services/public/FetcherService.js +21 -6
  28. package/dist/services/public/FetcherService.js.map +1 -1
  29. package/dist/services/public/TweetService.js +9 -6
  30. package/dist/services/public/TweetService.js.map +1 -1
  31. package/dist/services/public/UserService.d.ts +45 -0
  32. package/dist/services/public/UserService.js +211 -0
  33. package/dist/services/public/UserService.js.map +1 -1
  34. package/dist/types/args/PostArgs.d.ts +44 -1
  35. package/dist/types/raw/tweet/Post.d.ts +16 -1
  36. package/dist/types/raw/user/ChangePassword.d.ts +8 -0
  37. package/dist/types/raw/user/ChangePassword.js +3 -0
  38. package/dist/types/raw/user/ChangePassword.js.map +1 -0
  39. package/dist/types/raw/user/ProfileUpdate.d.ts +1 -0
  40. package/dist/types/raw/user/Settings.d.ts +21 -0
  41. package/dist/types/raw/user/Settings.js +4 -0
  42. package/dist/types/raw/user/Settings.js.map +1 -0
  43. package/package.json +4 -2
  44. package/src/collections/Extractors.ts +15 -3
  45. package/src/collections/Groups.ts +5 -0
  46. package/src/collections/Requests.ts +6 -0
  47. package/src/commands/User.ts +146 -0
  48. package/src/enums/Resource.ts +5 -0
  49. package/src/index.ts +2 -0
  50. package/src/models/args/PostArgs.ts +49 -1
  51. package/src/requests/Tweet.ts +59 -0
  52. package/src/requests/User.ts +63 -0
  53. package/src/services/public/FetcherService.ts +27 -7
  54. package/src/services/public/TweetService.ts +10 -7
  55. package/src/services/public/UserService.ts +265 -0
  56. package/src/types/args/PostArgs.ts +50 -1
  57. package/src/types/raw/tweet/Post.ts +19 -1
  58. package/src/types/raw/user/ChangePassword.ts +8 -0
  59. package/src/types/raw/user/ProfileUpdate.ts +1 -0
  60. package/src/types/raw/user/Settings.ts +23 -0
  61. package/.claude/settings.local.json +0 -9
@@ -1,3 +1,7 @@
1
+ import { readFileSync } from 'fs';
2
+ import { createInterface } from 'readline/promises';
3
+ import { Writable } from 'stream';
4
+
1
5
  import { Command, createCommand } from 'commander';
2
6
 
3
7
  import { RawAnalyticsGranularity, RawAnalyticsMetric } from '../enums/raw/Analytics';
@@ -353,9 +357,144 @@ function createUserCommand(rettiwt: Rettiwt): Command {
353
357
  }
354
358
  });
355
359
 
360
+ // Change Password
361
+ user.command('change-password')
362
+ .description('Change your account password')
363
+ .option('--show-new-key', 'Include rotated apiKey in the output')
364
+ .action(async (options?: UserPasswordChangeOptions) => {
365
+ try {
366
+ const initialApiKey = rettiwt.apiKey;
367
+ const currentPassword = await promptHidden('Current password: ');
368
+ const newPassword = await promptHidden('New password: ');
369
+ const confirmPassword = await promptHidden('Confirm new password: ');
370
+
371
+ if (newPassword !== confirmPassword) {
372
+ throw new Error('New password confirmation does not match');
373
+ }
374
+ if (newPassword === currentPassword) {
375
+ throw new Error('New password must be different from current password');
376
+ }
377
+
378
+ const result = await rettiwt.user.changePassword(currentPassword, newPassword);
379
+ const apiKeyUpdated = initialApiKey !== rettiwt.apiKey;
380
+ const response = {
381
+ success: result,
382
+ apiKeyUpdated: result ? apiKeyUpdated : false,
383
+ ...(options?.showNewKey ? { apiKey: rettiwt.apiKey } : {}),
384
+ };
385
+
386
+ output(response);
387
+ } catch (error) {
388
+ output(error);
389
+ }
390
+ });
391
+
392
+ // Change Username
393
+ user.command('change-username')
394
+ .description('Change your username')
395
+ .argument('<username>', 'The new username (with or without @)')
396
+ .action(async (username: string) => {
397
+ try {
398
+ const result = await rettiwt.user.changeUsername(username);
399
+ output(result);
400
+ } catch (error) {
401
+ output(error);
402
+ }
403
+ });
404
+
405
+ // Update Profile Banner
406
+ user.command('update-profile-banner')
407
+ .description('Update your profile banner from an image file path')
408
+ .argument('<path>', 'The path to the banner image file')
409
+ .action(async (path: string) => {
410
+ try {
411
+ const result = await rettiwt.user.updateProfileBanner(fileToBase64(path));
412
+ output(result);
413
+ } catch (error) {
414
+ output(error);
415
+ }
416
+ });
417
+
418
+ // Update Profile Image
419
+ user.command('update-profile-image')
420
+ .description('Update your profile image from an image file path')
421
+ .argument('<path>', 'The path to the profile image file')
422
+ .action(async (path: string) => {
423
+ try {
424
+ const result = await rettiwt.user.updateProfileImage(fileToBase64(path));
425
+ output(result);
426
+ } catch (error) {
427
+ output(error);
428
+ }
429
+ });
430
+
356
431
  return user;
357
432
  }
358
433
 
434
+ /**
435
+ * Reads a file and returns its base64 representation.
436
+ *
437
+ * @param path - The path to the file.
438
+ * @returns The base64 representation of the file contents.
439
+ */
440
+ function fileToBase64(path: string): string {
441
+ if (path.trim().length === 0) {
442
+ throw new Error('File path cannot be empty');
443
+ }
444
+
445
+ try {
446
+ return readFileSync(path).toString('base64');
447
+ } catch (error) {
448
+ if (error instanceof Error) {
449
+ throw new Error(`Could not read file at '${path}': ${error.message}`);
450
+ }
451
+
452
+ throw new Error(`Could not read file at '${path}'`);
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Prompts user for hidden input without echoing typed characters.
458
+ *
459
+ * @param query - The prompt text.
460
+ * @returns The provided value.
461
+ */
462
+ async function promptHidden(query: string): Promise<string> {
463
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
464
+ throw new Error('Password prompt requires an interactive terminal');
465
+ }
466
+
467
+ let queryShown = false;
468
+
469
+ const mutedOutput = new Writable({
470
+ write(chunk: Buffer | string, encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
471
+ const text = chunk.toString();
472
+
473
+ if (!queryShown) {
474
+ process.stdout.write(text);
475
+ queryShown = text.includes(query);
476
+ }
477
+
478
+ callback();
479
+ },
480
+ });
481
+
482
+ const input = createInterface({
483
+ input: process.stdin,
484
+ output: mutedOutput,
485
+ terminal: true,
486
+ });
487
+
488
+ try {
489
+ const value = await input.question(query);
490
+ process.stdout.write('\n');
491
+
492
+ return value;
493
+ } finally {
494
+ input.close();
495
+ }
496
+ }
497
+
359
498
  /**
360
499
  * The options for fetching user analytics.
361
500
  */
@@ -377,4 +516,11 @@ type UserProfileUpdateOptions = {
377
516
  description?: string;
378
517
  };
379
518
 
519
+ /**
520
+ * The options for changing account password.
521
+ */
522
+ type UserPasswordChangeOptions = {
523
+ showNewKey?: boolean;
524
+ };
525
+
380
526
  export default createUserCommand;
@@ -33,6 +33,7 @@ export enum ResourceType {
33
33
  TWEET_LIKE = 'TWEET_LIKE',
34
34
  TWEET_LIKERS = 'TWEET_LIKERS',
35
35
  TWEET_POST = 'TWEET_POST',
36
+ TWEET_POST_NOTE = 'TWEET_POST_NOTE',
36
37
  TWEET_REPLIES = 'TWEET_REPLIES',
37
38
  TWEET_RETWEET = 'TWEET_RETWEET',
38
39
  TWEET_RETWEETERS = 'TWEET_RETWEETERS',
@@ -70,4 +71,8 @@ export enum ResourceType {
70
71
  USER_TIMELINE_AND_REPLIES = 'USER_TIMELINE_AND_REPLIES',
71
72
  USER_UNFOLLOW = 'USER_UNFOLLOW',
72
73
  USER_PROFILE_UPDATE = 'USER_PROFILE_UPDATE',
74
+ USER_PROFILE_IMAGE_UPDATE = 'USER_PROFILE_IMAGE_UPDATE',
75
+ USER_PROFILE_BANNER_UPDATE = 'USER_PROFILE_BANNER_UPDATE',
76
+ USER_USERNAME_CHANGE = 'USER_USERNAME_CHANGE',
77
+ USER_PASSWORD_CHANGE = 'USER_PASSWORD_CHANGE',
73
78
  }
package/src/index.ts CHANGED
@@ -128,6 +128,8 @@ export { IUserTweetsResponse as IRawUserTweetsResponse } from './types/raw/user/
128
128
  export { IUserTweetsAndRepliesResponse as IRawUserTweetsAndRepliesResponse } from './types/raw/user/TweetsAndReplies';
129
129
  export { IUserUnfollowResponse as IRawUserUnfollowResponse } from './types/raw/user/Unfollow';
130
130
  export { IUserProfileUpdateResponse as IRawUserProfileUpdateResponse } from './types/raw/user/ProfileUpdate';
131
+ export { IUserSettingsResponse as IRawUserSettingsResponse } from './types/raw/user/Settings';
132
+ export { IUserChangePasswordResponse as IRawUserChangePasswordResponse } from './types/raw/user/ChangePassword';
131
133
  export * from './types/ErrorHandler';
132
134
  export * from './types/RettiwtConfig';
133
135
  export { IConversationTimelineResponse as IRawConversationTimelineResponse } from './types/raw/dm/Conversation';
@@ -1,4 +1,4 @@
1
- import { INewTweet, INewTweetMedia, IPostArgs, IUploadArgs } from '../../types/args/PostArgs';
1
+ import { IChangePasswordArgs, INewTweet, INewTweetMedia, IPostArgs, IUploadArgs } from '../../types/args/PostArgs';
2
2
 
3
3
  import { ProfileUpdateOptions } from './ProfileArgs';
4
4
 
@@ -8,12 +8,16 @@ import { ProfileUpdateOptions } from './ProfileArgs';
8
8
  * @public
9
9
  */
10
10
  export class PostArgs implements IPostArgs {
11
+ public changePassword?: ChangePasswordArgs;
11
12
  public conversationId?: string;
12
13
  public id?: string;
14
+ public profileBanner?: string;
15
+ public profileImage?: string;
13
16
  public profileOptions?: ProfileUpdateOptions;
14
17
  public tweet?: NewTweet;
15
18
  public upload?: UploadArgs;
16
19
  public userId?: string;
20
+ public username?: string;
17
21
 
18
22
  /**
19
23
  * @param resource - The resource to be posted.
@@ -24,8 +28,28 @@ export class PostArgs implements IPostArgs {
24
28
  this.tweet = args.tweet ? new NewTweet(args.tweet) : undefined;
25
29
  this.upload = args.upload ? new UploadArgs(args.upload) : undefined;
26
30
  this.userId = args.userId;
31
+ this.username = PostArgs._validateNonEmptyString(args.username, 'Username');
27
32
  this.conversationId = args.conversationId;
28
33
  this.profileOptions = args.profileOptions ? new ProfileUpdateOptions(args.profileOptions) : undefined;
34
+ this.profileImage = PostArgs._validateNonEmptyString(args.profileImage, 'Profile image');
35
+ this.profileBanner = PostArgs._validateNonEmptyString(args.profileBanner, 'Profile banner');
36
+ this.changePassword = args.changePassword ? new ChangePasswordArgs(args.changePassword) : undefined;
37
+ }
38
+
39
+ private static _validateNonEmptyString(value: unknown, fieldName: string): string | undefined {
40
+ if (value === undefined) {
41
+ return undefined;
42
+ }
43
+
44
+ if (typeof value !== 'string') {
45
+ throw new Error(`${fieldName} must be a string`);
46
+ }
47
+
48
+ if (value.trim().length === 0) {
49
+ throw new Error(`${fieldName} cannot be empty`);
50
+ }
51
+
52
+ return value;
29
53
  }
30
54
  }
31
55
 
@@ -91,3 +115,27 @@ export class UploadArgs implements IUploadArgs {
91
115
  this.id = args.id;
92
116
  }
93
117
  }
118
+
119
+ /**
120
+ * Validated password change arguments.
121
+ *
122
+ * @public
123
+ */
124
+ export class ChangePasswordArgs implements IChangePasswordArgs {
125
+ public currentPassword: string;
126
+ public newPassword: string;
127
+
128
+ public constructor(args: IChangePasswordArgs) {
129
+ if (!args.currentPassword || args.currentPassword.trim().length === 0) {
130
+ throw new Error('Current password cannot be empty');
131
+ }
132
+ if (!args.newPassword || args.newPassword.trim().length === 0) {
133
+ throw new Error('New password cannot be empty');
134
+ }
135
+ if (args.newPassword.length < 8) {
136
+ throw new Error('New password must be at least 8 characters long');
137
+ }
138
+ this.currentPassword = args.currentPassword;
139
+ this.newPassword = args.newPassword;
140
+ }
141
+ }
@@ -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.
@@ -326,6 +326,43 @@ export class UserRequests {
326
326
  };
327
327
  }
328
328
 
329
+ /**
330
+ * Returns the request to change the user's password.
331
+ *
332
+ * @param currentPassword - The current password.
333
+ * @param newPassword - The new password.
334
+ */
335
+ public static changePassword(currentPassword: string, newPassword: string): AxiosRequestConfig {
336
+ return {
337
+ method: 'post',
338
+ url: 'https://x.com/i/api/i/account/change_password.json',
339
+ data: qs.stringify({
340
+ /* eslint-disable @typescript-eslint/naming-convention */
341
+ current_password: currentPassword,
342
+ password: newPassword,
343
+ password_confirmation: newPassword,
344
+ /* eslint-enable @typescript-eslint/naming-convention */
345
+ }),
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Returns the request to change the user's username (screen_name).
351
+ *
352
+ * @param newUsername - The new username to set.
353
+ */
354
+ public static changeUsername(newUsername: string): AxiosRequestConfig {
355
+ return {
356
+ method: 'post',
357
+ url: 'https://x.com/i/api/1.1/account/settings.json',
358
+ data: qs.stringify({
359
+ /* eslint-disable @typescript-eslint/naming-convention */
360
+ screen_name: newUsername,
361
+ /* eslint-enable @typescript-eslint/naming-convention */
362
+ }),
363
+ };
364
+ }
365
+
329
366
  /**
330
367
  * @param id - The id of the user whose details are to be fetched.
331
368
  */
@@ -1224,4 +1261,30 @@ export class UserRequests {
1224
1261
  }),
1225
1262
  };
1226
1263
  }
1264
+
1265
+ /**
1266
+ * Returns the request to update the user's profile banner.
1267
+ *
1268
+ * @param bannerBase64 - The base64-encoded banner image data.
1269
+ */
1270
+ public static updateProfileBanner(bannerBase64: string): AxiosRequestConfig {
1271
+ return {
1272
+ method: 'post',
1273
+ url: 'https://x.com/i/api/1.1/account/update_profile_banner.json',
1274
+ data: qs.stringify({ banner: bannerBase64 }),
1275
+ };
1276
+ }
1277
+
1278
+ /**
1279
+ * Returns the request to update the user's profile image.
1280
+ *
1281
+ * @param imageBase64 - The base64-encoded image data.
1282
+ */
1283
+ public static updateProfileImage(imageBase64: string): AxiosRequestConfig {
1284
+ return {
1285
+ method: 'post',
1286
+ url: 'https://x.com/i/api/1.1/account/update_profile_image.json',
1287
+ data: qs.stringify({ image: imageBase64 }),
1288
+ };
1289
+ }
1227
1290
  }
@@ -1,4 +1,4 @@
1
- import axios, { AxiosError, isAxiosError } from 'axios';
1
+ import axios, { AxiosError, AxiosResponse, isAxiosError } from 'axios';
2
2
  import { Cookie } from 'cookiejar';
3
3
  import { JSDOM } from 'jsdom';
4
4
  import { ClientTransaction } from 'x-client-transaction-id';
@@ -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 HTTP response received.
262
262
  *
263
263
  * @example
264
264
  *
@@ -279,7 +279,10 @@ export class FetcherService {
279
279
  * });
280
280
  * ```
281
281
  */
282
- public async request<T = unknown>(resource: ResourceType, args: IFetchArgs | IPostArgs): Promise<T> {
282
+ protected async requestWithResponse<T = unknown>(
283
+ resource: ResourceType,
284
+ args: IFetchArgs | IPostArgs,
285
+ ): Promise<AxiosResponse<T>> {
283
286
  /** The current retry number. */
284
287
  let retry = 0;
285
288
 
@@ -325,7 +328,8 @@ export class FetcherService {
325
328
  await this._wait();
326
329
 
327
330
  // Getting the response body
328
- const responseData = (await axios<T>(config)).data;
331
+ const response = await axios<T>(config);
332
+ const responseData = response.data;
329
333
 
330
334
  // Check for Twitter API errors in response body
331
335
  // Type guard to check if response contains errors
@@ -348,8 +352,8 @@ export class FetcherService {
348
352
  throw new TwitterError(axiosError);
349
353
  }
350
354
 
351
- // Returning the reponse body
352
- return responseData;
355
+ // Returning the response
356
+ return response;
353
357
  } catch (err) {
354
358
  // If it's an error 404, retry
355
359
  if (isAxiosError(err) && err.status === 404) {
@@ -369,4 +373,20 @@ export class FetcherService {
369
373
  /** If request not successful even after retries, throw the error */
370
374
  throw error;
371
375
  }
376
+
377
+ /**
378
+ * Makes an HTTP request according to the given parameters.
379
+ *
380
+ * @param resource - The requested resource.
381
+ * @param args - The args to be used for the request.
382
+ *
383
+ * @typeParam T - The type of the returned response data.
384
+ *
385
+ * @returns The raw data response received.
386
+ */
387
+ public async request<T = unknown>(resource: ResourceType, args: IFetchArgs | IPostArgs): Promise<T> {
388
+ const response = await this.requestWithResponse<T>(resource, args);
389
+
390
+ return response.data;
391
+ }
372
392
  }
@@ -17,7 +17,7 @@ import { ITweetDetailsResponse } from '../../types/raw/tweet/Details';
17
17
  import { ITweetDetailsBulkResponse } from '../../types/raw/tweet/DetailsBulk';
18
18
  import { ITweetLikeResponse } from '../../types/raw/tweet/Like';
19
19
  import { ITweetLikersResponse } from '../../types/raw/tweet/Likers';
20
- import { ITweetPostResponse } from '../../types/raw/tweet/Post';
20
+ import { ITweetPostNoteResponse, ITweetPostResponse } from '../../types/raw/tweet/Post';
21
21
  import { ITweetRepliesResponse } from '../../types/raw/tweet/Replies';
22
22
  import { ITweetRetweetResponse } from '../../types/raw/tweet/Retweet';
23
23
  import { ITweetRetweetersResponse } from '../../types/raw/tweet/Retweeters';
@@ -342,15 +342,18 @@ export class TweetService extends FetcherService {
342
342
  * ```
343
343
  */
344
344
  public async post(options: INewTweet): Promise<string | undefined> {
345
- const resource = ResourceType.TWEET_POST;
345
+ // Use CreateNoteTweet endpoint for long-form tweets (X Premium, >280 chars)
346
+ if ((options.text?.length ?? 0) > 280) {
347
+ const response = await this.request<ITweetPostNoteResponse>(ResourceType.TWEET_POST_NOTE, {
348
+ tweet: options,
349
+ });
346
350
 
347
- // Posting the tweet
348
- const response = await this.request<ITweetPostResponse>(resource, { tweet: options });
351
+ return Extractors[ResourceType.TWEET_POST_NOTE](response);
352
+ }
349
353
 
350
- // Deserializing response
351
- const data = Extractors[resource](response);
354
+ const response = await this.request<ITweetPostResponse>(ResourceType.TWEET_POST, { tweet: options });
352
355
 
353
- return data;
356
+ return Extractors[ResourceType.TWEET_POST](response);
354
357
  }
355
358
 
356
359
  /**